diff --git a/README.md b/README.md index 14f7f5c..bfbc255 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,12 @@ Want support for another agent? [Open an issue](https://github.com/anthropics/sa ## Architecture -- Run the `sandbox-agent` daemon locally or inside a sandbox. -- The daemon spawns agents, normalizes their event streams into a universal schema, and exposes a single HTTP/SSE API. -- Clients (SDK, CLI, Inspector UI) all use the same `/v1` API for sessions and events. +![Agent Architecture Diagram](./agent-diagram.gif) + +The Sandbox Agent acts as a universal adapter between your client application and various coding agents (Claude Code, Codex, OpenCode, Amp). Each agent has its own adapter (e.g., `claude_adapter.rs`) that handles the translation between the universal API and the agent-specific interface. + +- **Embedded Mode**: Runs agents locally as subprocesses +- **Server Mode**: Runs as HTTP server from any sandbox provider See https://rivet.dev/docs/architecture for a deeper walkthrough. diff --git a/agent-diagram.gif b/agent-diagram.gif new file mode 100644 index 0000000..864681a Binary files /dev/null and b/agent-diagram.gif differ diff --git a/docs/logo/dark.svg b/docs/logo/dark.svg index 8b343cd..41cd342 100644 --- a/docs/logo/dark.svg +++ b/docs/logo/dark.svg @@ -1,21 +1,64 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/logo/light.svg b/docs/logo/light.svg index 03e62bf..9460187 100644 --- a/docs/logo/light.svg +++ b/docs/logo/light.svg @@ -1,21 +1,64 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/og.png b/og.png new file mode 100644 index 0000000..0ae02a6 Binary files /dev/null and b/og.png differ diff --git a/sandboxagent.svg b/sandboxagent.svg new file mode 100644 index 0000000..41cd342 --- /dev/null +++ b/sandboxagent.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/.astro/content-assets.mjs b/website/.astro/content-assets.mjs new file mode 100644 index 0000000..2b8b823 --- /dev/null +++ b/website/.astro/content-assets.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/website/.astro/content-modules.mjs b/website/.astro/content-modules.mjs new file mode 100644 index 0000000..2b8b823 --- /dev/null +++ b/website/.astro/content-modules.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/website/.astro/content.d.ts b/website/.astro/content.d.ts new file mode 100644 index 0000000..c0082cc --- /dev/null +++ b/website/.astro/content.d.ts @@ -0,0 +1,199 @@ +declare module 'astro:content' { + export interface RenderResult { + Content: import('astro/runtime/server/index.js').AstroComponentFactory; + headings: import('astro').MarkdownHeading[]; + remarkPluginFrontmatter: Record; + } + interface Render { + '.md': Promise; + } + + export interface RenderedContent { + html: string; + metadata?: { + imagePaths: Array; + [key: string]: unknown; + }; + } +} + +declare module 'astro:content' { + type Flatten = T extends { [K: string]: infer U } ? U : never; + + export type CollectionKey = keyof AnyEntryMap; + export type CollectionEntry = Flatten; + + export type ContentCollectionKey = keyof ContentEntryMap; + export type DataCollectionKey = keyof DataEntryMap; + + type AllValuesOf = T extends any ? T[keyof T] : never; + type ValidContentEntrySlug = AllValuesOf< + ContentEntryMap[C] + >['slug']; + + export type ReferenceDataEntry< + C extends CollectionKey, + E extends keyof DataEntryMap[C] = string, + > = { + collection: C; + id: E; + }; + export type ReferenceContentEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}) = string, + > = { + collection: C; + slug: E; + }; + export type ReferenceLiveEntry = { + collection: C; + id: string; + }; + + /** @deprecated Use `getEntry` instead. */ + export function getEntryBySlug< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + // Note that this has to accept a regular string too, for SSR + entrySlug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + + /** @deprecated Use `getEntry` instead. */ + export function getDataEntryById( + collection: C, + entryId: E, + ): Promise>; + + export function getCollection>( + collection: C, + filter?: (entry: CollectionEntry) => entry is E, + ): Promise; + export function getCollection( + collection: C, + filter?: (entry: CollectionEntry) => unknown, + ): Promise[]>; + + export function getLiveCollection( + collection: C, + filter?: LiveLoaderCollectionFilterType, + ): Promise< + import('astro').LiveDataCollectionResult, LiveLoaderErrorType> + >; + + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + entry: ReferenceContentEntry, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + entry: ReferenceDataEntry, + ): E extends keyof DataEntryMap[C] + ? Promise + : Promise | undefined>; + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + slug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + collection: C, + id: E, + ): E extends keyof DataEntryMap[C] + ? string extends keyof DataEntryMap[C] + ? Promise | undefined + : Promise + : Promise | undefined>; + export function getLiveEntry( + collection: C, + filter: string | LiveLoaderEntryFilterType, + ): Promise, LiveLoaderErrorType>>; + + /** Resolve an array of entry references from the same collection */ + export function getEntries( + entries: ReferenceContentEntry>[], + ): Promise[]>; + export function getEntries( + entries: ReferenceDataEntry[], + ): Promise[]>; + + export function render( + entry: AnyEntryMap[C][string], + ): Promise; + + export function reference( + collection: C, + ): import('astro/zod').ZodEffects< + import('astro/zod').ZodString, + C extends keyof ContentEntryMap + ? ReferenceContentEntry> + : ReferenceDataEntry + >; + // Allow generic `string` to avoid excessive type errors in the config + // if `dev` is not running to update as you edit. + // Invalid collection names will be caught at build time. + export function reference( + collection: C, + ): import('astro/zod').ZodEffects; + + type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; + type InferEntrySchema = import('astro/zod').infer< + ReturnTypeOrOriginal['schema']> + >; + + type ContentEntryMap = { + + }; + + type DataEntryMap = { + + }; + + type AnyEntryMap = ContentEntryMap & DataEntryMap; + + type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< + infer TData, + infer TEntryFilter, + infer TCollectionFilter, + infer TError + > + ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } + : { data: never; entryFilter: never; collectionFilter: never; error: never }; + type ExtractDataType = ExtractLoaderTypes['data']; + type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; + type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; + type ExtractErrorType = ExtractLoaderTypes['error']; + + type LiveLoaderDataType = + LiveContentConfig['collections'][C]['schema'] extends undefined + ? ExtractDataType + : import('astro/zod').infer< + Exclude + >; + type LiveLoaderEntryFilterType = + ExtractEntryFilterType; + type LiveLoaderCollectionFilterType = + ExtractCollectionFilterType; + type LiveLoaderErrorType = ExtractErrorType< + LiveContentConfig['collections'][C]['loader'] + >; + + export type ContentConfig = typeof import("../src/content.config.mjs"); + export type LiveContentConfig = never; +} diff --git a/website/.astro/data-store.json b/website/.astro/data-store.json new file mode 100644 index 0000000..e16f873 --- /dev/null +++ b/website/.astro/data-store.json @@ -0,0 +1 @@ +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.15","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] \ No newline at end of file diff --git a/website/.astro/settings.json b/website/.astro/settings.json new file mode 100644 index 0000000..637fb04 --- /dev/null +++ b/website/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "_variables": { + "lastUpdateCheck": 1769384167008 + } +} \ No newline at end of file diff --git a/website/.astro/types.d.ts b/website/.astro/types.d.ts new file mode 100644 index 0000000..03d7cc4 --- /dev/null +++ b/website/.astro/types.d.ts @@ -0,0 +1,2 @@ +/// +/// \ No newline at end of file diff --git a/website/astro.config.mjs b/website/astro.config.mjs new file mode 100644 index 0000000..41e24e0 --- /dev/null +++ b/website/astro.config.mjs @@ -0,0 +1,11 @@ +import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; +import tailwind from '@astrojs/tailwind'; + +export default defineConfig({ + output: 'static', + integrations: [ + react(), + tailwind() + ] +}); diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..e1ac5cb --- /dev/null +++ b/website/package.json @@ -0,0 +1,27 @@ +{ + "name": "sandbox-agent-website", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/react": "^4.2.0", + "@astrojs/tailwind": "^6.0.0", + "astro": "^5.1.0", + "framer-motion": "^12.0.0", + "lucide-react": "^0.469.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^3.4.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" + } +} diff --git a/website/public/favicon.svg b/website/public/favicon.svg new file mode 100644 index 0000000..b785c73 --- /dev/null +++ b/website/public/favicon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/website/public/logos/daytona.png b/website/public/logos/daytona.png new file mode 100644 index 0000000..afd8aa5 Binary files /dev/null and b/website/public/logos/daytona.png differ diff --git a/website/public/logos/daytona.svg b/website/public/logos/daytona.svg new file mode 100644 index 0000000..286fc93 --- /dev/null +++ b/website/public/logos/daytona.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/website/public/logos/e2b.png b/website/public/logos/e2b.png new file mode 100644 index 0000000..afce0f0 Binary files /dev/null and b/website/public/logos/e2b.png differ diff --git a/website/public/logos/e2b.svg b/website/public/logos/e2b.svg new file mode 100644 index 0000000..3063e5e --- /dev/null +++ b/website/public/logos/e2b.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/public/logos/vercel.svg b/website/public/logos/vercel.svg new file mode 100644 index 0000000..ef92695 --- /dev/null +++ b/website/public/logos/vercel.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/public/rivet-icon.svg b/website/public/rivet-icon.svg new file mode 100644 index 0000000..287c425 --- /dev/null +++ b/website/public/rivet-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/public/rivet-logo-text-white.svg b/website/public/rivet-logo-text-white.svg new file mode 100644 index 0000000..a7a146c --- /dev/null +++ b/website/public/rivet-logo-text-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/website/public/sandboxsdklogo.svg b/website/public/sandboxsdklogo.svg new file mode 100644 index 0000000..173b0ab --- /dev/null +++ b/website/public/sandboxsdklogo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/website/src/components/CTASection.tsx b/website/src/components/CTASection.tsx new file mode 100644 index 0000000..e689375 --- /dev/null +++ b/website/src/components/CTASection.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowRight, Terminal, Check } from 'lucide-react'; + +const CTA_TITLES = [ + 'Run any coding agent in any sandbox — with Sandbox Agent.', + 'One API for all coding agents — powered by Sandbox Agent.', + 'Claude Code, Codex, Amp — unified with Sandbox Agent.', + 'Swap agents without refactoring — thanks to Sandbox Agent.', + 'Agentic backends, zero complexity — with Sandbox Agent.', + 'Manage transcripts, maintain state — powered by Sandbox Agent.', + 'Your agents deserve a universal API — Sandbox Agent delivers.', + 'Build agentic sandboxes that scale — with Sandbox Agent.', +]; + +function AnimatedCTATitle() { + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentIndex(prev => (prev + 1) % CTA_TITLES.length); + }, 3000); + + return () => clearInterval(interval); + }, []); + + return ( +

+ + + {CTA_TITLES[currentIndex]} + + +

+ ); +} + +const CopyInstallButton = () => { + const [copied, setCopied] = useState(false); + const installCommand = 'npx rivet-dev/sandbox-agent'; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(installCommand); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + return ( + + ); +}; + +export function CTASection() { + return ( +
+
+ +
+
+ +
+ + API for coding agents.
+ Deploy anywhere. Swap agents on demand. +
+ + + Read the Docs + + + + +
+
+ ); +} diff --git a/website/src/components/FeatureGrid.tsx b/website/src/components/FeatureGrid.tsx new file mode 100644 index 0000000..7282a9d --- /dev/null +++ b/website/src/components/FeatureGrid.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { Workflow, Server, Database, Zap, Globe } from 'lucide-react'; +import { FeatureIcon } from './ui/FeatureIcon'; +import { CopyButton } from './ui/CopyButton'; + +export function FeatureGrid() { + return ( +
+
+
+

+ Full feature coverage.
+ Solving the fundamental friction points. +

+

+ Everything you need to ship agents in sandboxes in record time. +

+
+ +
+ {/* Universal Agent API - Span 7 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+
+ +

Universal Agent API

+
+

+ Coding agents like Claude Code and Amp have custom scaffolds. We provide a single, + unified interface to swap between engines effortlessly. +

+
+ +
+
+ agent.spawn() + agent.terminate() + agent.logs() +
+
+
+
+ + Swapping provider to "daytona"... + +
+
+
+ Hot-reloading sandbox context... +
+
// No code changes required
+
+
+
+ + {/* Server Mode - Span 5 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+ +

Server Mode

+
+

+ Run as an HTTP server within any sandbox. One command to bridge your agent to the + local environment. +

+
+ $ sandbox-agent serve --port 4000 +
+
+ + {/* Universal Schema - Span 5 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+ +

Universal Schema

+
+

+ Standardized session JSON to store and replay agent actions. Built-in adapters for + Postgres and ClickHouse. +

+
+ + {/* Rust Binary - Span 8 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+
+ +

Rust Binary

+
+

+ Statically-linked binary. Zero dependencies. 4MB total size. Instant startup with no runtime overhead. +

+
+
+
+
+ + Quick Install +
+
+ $ + curl -sSL https://sandboxagent.dev/install | sh +
+ +
+
+
+
+
+ + {/* Provider Agnostic - Span 4 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+ +

Provider Agnostic

+
+

+ Seamless support for E2B, Daytona, Vercel Sandboxes, and custom Docker. +

+
+
+
+
+ ); +} diff --git a/website/src/components/Footer.tsx b/website/src/components/Footer.tsx new file mode 100644 index 0000000..7d93ae4 --- /dev/null +++ b/website/src/components/Footer.tsx @@ -0,0 +1,135 @@ +'use client'; + +const footer = { + products: [ + { name: 'Actors', href: 'https://rivet.dev/docs/actors' }, + { name: 'Sandbox Agent SDK', href: '/docs' }, + ], + developers: [ + { name: 'Documentation', href: '/docs' }, + { name: 'Changelog', href: '/changelog' }, + { name: 'Blog', href: 'https://rivet.dev/blog' }, + ], + legal: [ + { name: 'Terms', href: 'https://rivet.dev/terms' }, + { name: 'Privacy Policy', href: 'https://rivet.dev/privacy' }, + { name: 'Acceptable Use', href: 'https://rivet.dev/acceptable-use' }, + ], + social: [ + { + name: 'Discord', + href: 'https://discord.gg/sandbox-agent', + icon: ( + + + + ), + }, + { + name: 'GitHub', + href: 'https://github.com/rivet-dev/sandbox-agent', + icon: ( + + + + ), + }, + { + name: 'Twitter', + href: 'https://x.com/rivet_dev', + icon: ( + + + + ), + }, + ], +}; + +export function Footer() { + return ( + + ); +} diff --git a/website/src/components/GitHubStars.tsx b/website/src/components/GitHubStars.tsx new file mode 100644 index 0000000..38799aa --- /dev/null +++ b/website/src/components/GitHubStars.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface GitHubStarsProps extends React.AnchorHTMLAttributes { + repo?: string; +} + +function formatNumber(num: number): string { + if (num >= 1000) { + return `${(num / 1000).toFixed(1)}k`; + } + return num.toString(); +} + +export function GitHubStars({ + repo = 'rivet-dev/sandbox-agent', + className, + ...props +}: GitHubStarsProps) { + const [stars, setStars] = useState(null); + + useEffect(() => { + const cacheKey = `github-stars-${repo}`; + const cachedData = sessionStorage.getItem(cacheKey); + + if (cachedData) { + const { stars: cachedStars, timestamp } = JSON.parse(cachedData); + // Check if cache is less than 5 minutes old + if (Date.now() - timestamp < 5 * 60 * 1000) { + setStars(cachedStars); + return; + } + } + + fetch(`https://api.github.com/repos/${repo}`) + .then((response) => { + if (!response.ok) throw new Error('Failed to fetch'); + return response.json(); + }) + .then((data) => { + const newStars = data.stargazers_count; + setStars(newStars); + sessionStorage.setItem( + cacheKey, + JSON.stringify({ + stars: newStars, + timestamp: Date.now(), + }), + ); + }) + .catch((err) => { + console.error('Failed to fetch stars', err); + }); + }, [repo]); + + return ( + + + + + + {stars ? `${formatNumber(stars)} Stars` : 'GitHub'} + + + ); +} diff --git a/website/src/components/Hero.tsx b/website/src/components/Hero.tsx new file mode 100644 index 0000000..a8a808d --- /dev/null +++ b/website/src/components/Hero.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { useState } from 'react'; +import { Terminal, Check, ArrowRight } from 'lucide-react'; + +const CopyInstallButton = () => { + const [copied, setCopied] = useState(false); + const installCommand = 'npx rivet-dev/sandbox-agent'; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(installCommand); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + return ( + + ); +}; + +export function Hero() { + return ( +
+
+
+
+

+ API for
+ Sandbox Agents +

+

+ One API to run Claude Code, Codex, and Amp inside any sandbox. Manage transcripts, maintain state, and swap agents with zero refactoring. +

+ + + +
+ + Supported Sandbox Providers + +
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
example_agent.ts
+
+
+ + import{' '} + {'{ SandboxAgent }'}{' '} + from{' '} + "@sandbox/sdk"; + + + // Start Claude Code in an E2B sandbox + + + const{' '} + agent = + await{' '} + SandboxAgent. + spawn + {'({'} + + + provider: + "e2b", + + + engine: + "claude-code" + + + {'}'}); + +
+ + const{' '} + transcript = + await{' '} + agent. + getTranscript + (); + +
+
+
+
+
+
+
+ ); +} + +function CodeLine({ num, children }: { num: string; children: React.ReactNode }) { + return ( +
+ {num} + {children} +
+ ); +} + +function DaytonaLogo() { + return ( + + Daytona + + ); +} + +function E2BLogo() { + return ( + + E2B + + ); +} + +function VercelLogo() { + return ( + + + + + + ); +} diff --git a/website/src/components/Integrations.tsx b/website/src/components/Integrations.tsx new file mode 100644 index 0000000..71bb915 --- /dev/null +++ b/website/src/components/Integrations.tsx @@ -0,0 +1,35 @@ +'use client'; + +const integrations = [ + 'Daytona', + 'E2B', + 'AI SDK', + 'Anthropic', + 'OpenAI', + 'Docker', + 'Fly.io', + 'AWS Nitro', + 'Postgres', + 'ClickHouse', + 'Rivet', +]; + +export function Integrations() { + return ( +
+
+

Works with your stack

+
+ {integrations.map((item) => ( +
+ {item} +
+ ))} +
+
+
+ ); +} diff --git a/website/src/components/Navigation.tsx b/website/src/components/Navigation.tsx new file mode 100644 index 0000000..3c328b0 --- /dev/null +++ b/website/src/components/Navigation.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Menu, X } from 'lucide-react'; +import { GitHubStars } from './GitHubStars'; + +function NavItem({ href, children }: { href: string; children: React.ReactNode }) { + return ( +
+ + {children} + +
+ ); +} + +export function Navigation() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 20); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( +
+
+ + + {/* Mobile menu */} + {mobileMenuOpen && ( +
+ +
+ )} +
+ ); +} diff --git a/website/src/components/PainPoints.tsx b/website/src/components/PainPoints.tsx new file mode 100644 index 0000000..fd9934a --- /dev/null +++ b/website/src/components/PainPoints.tsx @@ -0,0 +1,224 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const frictions = [ + { + number: '01', + title: 'Fragmented Agent Scaffolds', + description: + 'Every coding agent (Claude Code, Amp, OpenCode) uses proprietary plumbing. Swapping agents means rewriting your entire infrastructure bridge.', + solution: 'Unified control plane for all agent engines.', + visual: ( +
+
+
+ Claude Bridge +
+
+
+ Amp Bridge +
+
+
+ API +
+
+
+ 01{' '} + agent + . + spawn + ( + "claude-code" + ) +
+
+ 02{' '} + agent + . + spawn + ( + "amp" + ) +
+
// Exactly same methods
+
+
+ ), + accentColor: 'orange', + }, + { + number: '02', + title: 'Deploy Anywhere', + description: + 'Sandbox providers like E2B, Daytona, and Vercel each have unique strengths. Building integrations for each one from scratch is tedious.', + solution: 'One SDK, every provider. Deploy to any sandbox platform with a single config change.', + visual: ( +
+
+
+ E2B +
+
+
+
+ Daytona +
+
+
+
+ Vercel +
+
+
+
# Works with all providers
+
+ SANDBOX_PROVIDER + = + "daytona" +
+
+
+ ), + accentColor: 'purple', + }, + { + number: '03', + title: 'Transient State', + description: + 'Transcripts and session data are usually lost when the sandbox dies. Debugging becomes impossible.', + solution: 'Standardized session JSON. Stream events to your own storage in real-time.', + visual: ( +
+
+
# Session persisted automatically
+
+
+ "events" + : [ +
+
+ {'{ '} + "type" + : + "tool_call" + {' }'} +
+
+ {'{ '} + "type" + : + "message" + {' }'} +
+
]
+
+
+ + Streaming to Rivet Actors +
+
+
+ ), + accentColor: 'blue', + }, +]; + +const accentStyles = { + orange: { + gradient: 'from-orange-500/20', + border: 'border-orange-500/30', + glow: 'rgba(255,79,0,0.15)', + number: 'text-orange-500', + }, + purple: { + gradient: 'from-purple-500/20', + border: 'border-purple-500/30', + glow: 'rgba(168,85,247,0.15)', + number: 'text-purple-500', + }, + blue: { + gradient: 'from-blue-500/20', + border: 'border-blue-500/30', + glow: 'rgba(59,130,246,0.15)', + number: 'text-blue-500', + }, +}; + +export function PainPoints() { + return ( +
+
+ +

+ Building coding agents is hard. +

+

+ Integrating coding agents into your product means dealing with fragmented tooling, + provider-specific APIs, and ephemeral state. +

+
+ +
+ {frictions.map((friction, index) => { + const styles = accentStyles[friction.accentColor as keyof typeof accentStyles]; + return ( + + {/* Top shine */} +
+ + {/* Hover glow */} +
+ + {/* Corner highlight */} +
+ +
+ {/* Friction number */} +
+ + Friction #{friction.number} + +
+ + {/* Title */} +

{friction.title}

+ + {/* Description */} +

{friction.description}

+ + {/* Solution */} +
+

{friction.solution}

+
+ + {/* Visual */} + {friction.visual} +
+ + ); + })} +
+
+
+ ); +} diff --git a/website/src/components/ProblemsSolved.tsx b/website/src/components/ProblemsSolved.tsx new file mode 100644 index 0000000..893d9c8 --- /dev/null +++ b/website/src/components/ProblemsSolved.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { Workflow, Database, Server } from 'lucide-react'; +import { FeatureIcon } from './ui/FeatureIcon'; + +const problems = [ + { + title: 'Universal Agent API', + desc: 'Coding agents like Claude Code and Amp have custom scaffolds. We provide a single API to swap between them effortlessly.', + icon: Workflow, + color: 'text-accent', + }, + { + title: 'Universal Transcripts', + desc: 'Maintaining agent history is hard when the agent manages its own session. Our schema makes retrieval and storage simple.', + icon: Database, + color: 'text-purple-400', + }, + { + title: 'Agents in Sandboxes', + desc: 'Run a simple curl command inside any sandbox to spawn an HTTP server that bridges the agent to your system.', + icon: Server, + color: 'text-green-400', + }, +]; + +export function ProblemsSolved() { + return ( +
+
+
+

Why Sandbox Agent SDK?

+

+ Solving the three fundamental friction points of agentic software development. +

+
+ +
+ {problems.map((item, idx) => ( +
+ +

{item.title}

+

{item.desc}

+
+ ))} +
+
+
+ ); +} diff --git a/website/src/components/ui/Badge.tsx b/website/src/components/ui/Badge.tsx new file mode 100644 index 0000000..9023b7f --- /dev/null +++ b/website/src/components/ui/Badge.tsx @@ -0,0 +1,11 @@ +interface BadgeProps { + children: React.ReactNode; +} + +export function Badge({ children }: BadgeProps) { + return ( + + {children} + + ); +} diff --git a/website/src/components/ui/Button.tsx b/website/src/components/ui/Button.tsx new file mode 100644 index 0000000..4706fdf --- /dev/null +++ b/website/src/components/ui/Button.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from 'react'; + +interface ButtonProps { + children: ReactNode; + variant?: 'primary' | 'secondary' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + href?: string; + onClick?: () => void; + className?: string; +} + +export function Button({ + children, + variant = 'primary', + size = 'md', + href, + onClick, + className = '' +}: ButtonProps) { + const baseStyles = 'inline-flex items-center justify-center font-bold rounded-lg transition-all'; + + const variants = { + primary: 'bg-white text-black hover:bg-zinc-200', + secondary: 'bg-zinc-900 border border-white/10 text-white hover:bg-zinc-800', + ghost: 'text-zinc-400 hover:text-white', + }; + + const sizes = { + sm: 'h-9 px-4 text-sm', + md: 'h-12 px-8 text-sm', + lg: 'h-14 px-10 text-base', + }; + + const classes = `${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`; + + if (href) { + return ( + + {children} + + ); + } + + return ( + + ); +} diff --git a/website/src/components/ui/CopyButton.tsx b/website/src/components/ui/CopyButton.tsx new file mode 100644 index 0000000..a6e5b6a --- /dev/null +++ b/website/src/components/ui/CopyButton.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useState } from 'react'; +import { Copy, CheckCircle2 } from 'lucide-react'; + +interface CopyButtonProps { + text: string; +} + +export function CopyButton({ text }: CopyButtonProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} diff --git a/website/src/components/ui/FeatureIcon.tsx b/website/src/components/ui/FeatureIcon.tsx new file mode 100644 index 0000000..e6ed276 --- /dev/null +++ b/website/src/components/ui/FeatureIcon.tsx @@ -0,0 +1,23 @@ +import type { LucideIcon } from 'lucide-react'; + +interface FeatureIconProps { + icon: LucideIcon; + color?: string; + bgColor?: string; + hoverBgColor?: string; + glowShadow?: string; +} + +export function FeatureIcon({ + icon: Icon, + color = 'text-accent', + bgColor = 'bg-accent/10', + hoverBgColor = 'group-hover:bg-accent/20', + glowShadow = 'group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]' +}: FeatureIconProps) { + return ( +
+ +
+ ); +} diff --git a/website/src/layouts/Layout.astro b/website/src/layouts/Layout.astro new file mode 100644 index 0000000..741169c --- /dev/null +++ b/website/src/layouts/Layout.astro @@ -0,0 +1,39 @@ +--- +interface Props { + title: string; + description?: string; +} + +const { title, description = "API for running Claude Code, Codex, and Amp inside any sandbox." } = Astro.props; +--- + + + + + + + + + + + + {title} + + + + + + + + + + + + + + + + + diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro new file mode 100644 index 0000000..f63858e --- /dev/null +++ b/website/src/pages/index.astro @@ -0,0 +1,24 @@ +--- +import Layout from '../layouts/Layout.astro'; +import { Navigation } from '../components/Navigation'; +import { Hero } from '../components/Hero'; +import { PainPoints } from '../components/PainPoints'; +import { FeatureGrid } from '../components/FeatureGrid'; +import { Integrations } from '../components/Integrations'; +import { CTASection } from '../components/CTASection'; +import { Footer } from '../components/Footer'; +--- + + +
+ +
+ + + + + +
+
+
diff --git a/website/src/styles/global.css b/website/src/styles/global.css new file mode 100644 index 0000000..8ed649b --- /dev/null +++ b/website/src/styles/global.css @@ -0,0 +1,40 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-black text-white antialiased; + font-family: 'Open Sans', system-ui, sans-serif; + } + + ::selection { + @apply bg-accent/30 text-white; + } + + ::-webkit-scrollbar { + @apply w-2; + } + + ::-webkit-scrollbar-track { + @apply bg-zinc-900; + } + + ::-webkit-scrollbar-thumb { + @apply bg-zinc-700 rounded-full; + } + + ::-webkit-scrollbar-thumb:hover { + @apply bg-zinc-600; + } +} + +@layer components { + .glass { + @apply bg-white/[0.02] backdrop-blur-md border border-white/10; + } + + .glass-hover { + @apply hover:bg-white/[0.04] hover:border-white/20 transition-all; + } +} diff --git a/website/tailwind.config.mjs b/website/tailwind.config.mjs new file mode 100644 index 0000000..142adbc --- /dev/null +++ b/website/tailwind.config.mjs @@ -0,0 +1,26 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], + theme: { + extend: { + colors: { + accent: '#FF4500', + }, + fontFamily: { + sans: ['Open Sans', 'system-ui', 'sans-serif'], + mono: ['JetBrains Mono', 'monospace'], + }, + animation: { + 'fade-in-up': 'fade-in-up 0.6s ease-out forwards', + 'pulse-slow': 'pulse 3s ease-in-out infinite', + }, + keyframes: { + 'fade-in-up': { + '0%': { opacity: '0', transform: 'translateY(20px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + }, + }, + }, + plugins: [], +}; diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 0000000..b7243b9 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + } +}