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/frontend/packages/website/.astro/content-assets.mjs b/frontend/packages/website/.astro/content-assets.mjs new file mode 100644 index 0000000..2b8b823 --- /dev/null +++ b/frontend/packages/website/.astro/content-assets.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/frontend/packages/website/.astro/content-modules.mjs b/frontend/packages/website/.astro/content-modules.mjs new file mode 100644 index 0000000..2b8b823 --- /dev/null +++ b/frontend/packages/website/.astro/content-modules.mjs @@ -0,0 +1 @@ +export default new Map(); \ No newline at end of file diff --git a/frontend/packages/website/.astro/content.d.ts b/frontend/packages/website/.astro/content.d.ts new file mode 100644 index 0000000..c0082cc --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/.astro/data-store.json b/frontend/packages/website/.astro/data-store.json new file mode 100644 index 0000000..e16f873 --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/.astro/settings.json b/frontend/packages/website/.astro/settings.json new file mode 100644 index 0000000..02040a5 --- /dev/null +++ b/frontend/packages/website/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "_variables": { + "lastUpdateCheck": 1769576078224 + } +} \ No newline at end of file diff --git a/frontend/packages/website/.astro/types.d.ts b/frontend/packages/website/.astro/types.d.ts new file mode 100644 index 0000000..f964fe0 --- /dev/null +++ b/frontend/packages/website/.astro/types.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/packages/website/Dockerfile b/frontend/packages/website/Dockerfile index c1119b0..0c2b315 100644 --- a/frontend/packages/website/Dockerfile +++ b/frontend/packages/website/Dockerfile @@ -2,21 +2,20 @@ FROM node:22-alpine AS build WORKDIR /app RUN npm install -g pnpm -# Copy package files for all workspaces -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY frontend/packages/website/package.json ./frontend/packages/website/ +# Copy website package +COPY frontend/packages/website/package.json ./ # Install dependencies -RUN pnpm install --filter @sandbox-agent/website... +RUN pnpm install # Copy website source -COPY frontend/packages/website ./frontend/packages/website +COPY frontend/packages/website/ . # Build -RUN pnpm --filter @sandbox-agent/website build +RUN pnpm build FROM caddy:alpine -COPY --from=build /app/frontend/packages/website/dist /srv +COPY --from=build /app/dist /srv RUN cat > /etc/caddy/Caddyfile <<'EOF' :80 { root * /srv diff --git a/frontend/packages/website/astro.config.mjs b/frontend/packages/website/astro.config.mjs new file mode 100644 index 0000000..41e24e0 --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/package.json b/frontend/packages/website/package.json index 270c6ce..aef9c0b 100644 --- a/frontend/packages/website/package.json +++ b/frontend/packages/website/package.json @@ -1,15 +1,29 @@ { "name": "@sandbox-agent/website", "private": true, - "version": "0.0.0", + "version": "0.0.1", "license": "Apache-2.0", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" + "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": { - "vite": "^5.4.7" + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" } } diff --git a/frontend/packages/website/public/favicon.svg b/frontend/packages/website/public/favicon.svg new file mode 100644 index 0000000..287c425 --- /dev/null +++ b/frontend/packages/website/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/website/public/logos/amp.svg b/frontend/packages/website/public/logos/amp.svg new file mode 100644 index 0000000..c2dfe36 --- /dev/null +++ b/frontend/packages/website/public/logos/amp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/website/public/logos/claude.svg b/frontend/packages/website/public/logos/claude.svg new file mode 100644 index 0000000..879ad81 --- /dev/null +++ b/frontend/packages/website/public/logos/claude.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/packages/website/public/logos/daytona.png b/frontend/packages/website/public/logos/daytona.png new file mode 100644 index 0000000..afd8aa5 Binary files /dev/null and b/frontend/packages/website/public/logos/daytona.png differ diff --git a/frontend/packages/website/public/logos/daytona.svg b/frontend/packages/website/public/logos/daytona.svg new file mode 100644 index 0000000..286fc93 --- /dev/null +++ b/frontend/packages/website/public/logos/daytona.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/packages/website/public/logos/e2b.png b/frontend/packages/website/public/logos/e2b.png new file mode 100644 index 0000000..afce0f0 Binary files /dev/null and b/frontend/packages/website/public/logos/e2b.png differ diff --git a/frontend/packages/website/public/logos/e2b.svg b/frontend/packages/website/public/logos/e2b.svg new file mode 100644 index 0000000..3063e5e --- /dev/null +++ b/frontend/packages/website/public/logos/e2b.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/packages/website/public/logos/openai.svg b/frontend/packages/website/public/logos/openai.svg new file mode 100644 index 0000000..ce2c403 --- /dev/null +++ b/frontend/packages/website/public/logos/openai.svg @@ -0,0 +1,2 @@ + +OpenAI icon \ No newline at end of file diff --git a/frontend/packages/website/public/logos/opencode.svg b/frontend/packages/website/public/logos/opencode.svg new file mode 100644 index 0000000..c2404f2 --- /dev/null +++ b/frontend/packages/website/public/logos/opencode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/website/public/logos/sanboxagent.svg b/frontend/packages/website/public/logos/sanboxagent.svg new file mode 100644 index 0000000..f4704bd --- /dev/null +++ b/frontend/packages/website/public/logos/sanboxagent.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/website/public/logos/sandboxagent.svg b/frontend/packages/website/public/logos/sandboxagent.svg new file mode 100644 index 0000000..f4704bd --- /dev/null +++ b/frontend/packages/website/public/logos/sandboxagent.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/website/public/logos/sourcegraph.svg b/frontend/packages/website/public/logos/sourcegraph.svg new file mode 100644 index 0000000..e69de29 diff --git a/frontend/packages/website/public/logos/vercel.svg b/frontend/packages/website/public/logos/vercel.svg new file mode 100644 index 0000000..ef92695 --- /dev/null +++ b/frontend/packages/website/public/logos/vercel.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/packages/website/public/rivet-icon.svg b/frontend/packages/website/public/rivet-icon.svg new file mode 100644 index 0000000..287c425 --- /dev/null +++ b/frontend/packages/website/public/rivet-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/website/public/rivet-logo-text-white.svg b/frontend/packages/website/public/rivet-logo-text-white.svg new file mode 100644 index 0000000..a7a146c --- /dev/null +++ b/frontend/packages/website/public/rivet-logo-text-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/packages/website/src/components/CTASection.tsx b/frontend/packages/website/src/components/CTASection.tsx new file mode 100644 index 0000000..082e6d7 --- /dev/null +++ b/frontend/packages/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 = [ + 'Control any coding agent with one SDK.', + 'Claude Code, Codex, OpenCode, Amp — unified.', + 'Swap agents without refactoring.', + 'Universal events. Universal sessions.', + 'Stream, store, and replay agent transcripts.', + 'Human-in-the-loop, built in.', + 'One SDK. Every coding agent.', + 'Deploy anywhere. Same API everywhere.', +]; + +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 = 'curl -sSL https://sandboxagent.dev/install | sh'; + + 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 ( +
+
+ +
+
+ +
+ + Universal SDK for coding agents.
+ Control Claude Code, Codex, OpenCode, and Amp with one API. +
+ + + Read the Docs + + + + +
+
+ ); +} diff --git a/frontend/packages/website/src/components/FAQ.tsx b/frontend/packages/website/src/components/FAQ.tsx new file mode 100644 index 0000000..e5bf54b --- /dev/null +++ b/frontend/packages/website/src/components/FAQ.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +const faqs = [ + { + question: 'Does this replace the Vercel AI SDK?', + answer: + "No, they're complementary. AI SDK is for building chat interfaces and calling LLMs. This SDK is for controlling autonomous coding agents that write code and run commands. Use AI SDK for your UI, use this when you need an agent to actually code.", + }, + { + question: 'Which coding agents are supported?', + answer: + 'Claude Code, Codex, OpenCode, and Amp. The SDK normalizes their APIs so you can swap between them without changing your code.', + }, + { + question: 'How is session data persisted?', + answer: + "Events stream in a universal JSON schema. Persist them anywhere. We have adapters for Postgres and ClickHouse, or use Rivet Actors for managed stateful storage.", + }, + { + question: 'Can I run this locally or does it require a sandbox provider?', + answer: + "Both. Run locally for development, deploy to E2B, Daytona, Vercel, or Docker for production.", + }, + { + question: 'Is this open source?', + answer: + "Yes, MIT licensed. Code is on GitHub.", + }, +]; + +function FAQItem({ question, answer }: { question: string; answer: string }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + {isOpen && ( + +

+ + )} + +

+ ); +} + +export function FAQ() { + return ( +
+
+
+

+ Frequently Asked Questions +

+

+ Common questions about the Coding Agent SDK. +

+
+ +
+ {faqs.map((faq, index) => ( + + ))} +
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/FeatureGrid.tsx b/frontend/packages/website/src/components/FeatureGrid.tsx new file mode 100644 index 0000000..c5bcdca --- /dev/null +++ b/frontend/packages/website/src/components/FeatureGrid.tsx @@ -0,0 +1,329 @@ +'use client'; + +import { Workflow, Server, Database, Download, Globe } from 'lucide-react'; +import { FeatureIcon } from './ui/FeatureIcon'; +import { CopyButton } from './ui/CopyButton'; + +function AgentLogo({ name, color, src }: { name: string; color: string; src?: string }) { + return ( +
+ {src ? ( + {name} + ) : ( +
+ {name[0]} +
+ )} + {name} +
+ ); +} + +function ProviderLogo({ name, src }: { name: string; src?: string }) { + return ( +
+ {src ? ( + {name} + ) : ( +
+ D +
+ )} + {name} +
+ ); +} + +export function FeatureGrid() { + return ( +
+
+
+

+ Full feature coverage. +

+

+ Available as an HTTP API or TypeScript SDK. +

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

Universal Agent API

+
+

+ Claude Code, Codex, OpenCode, and Amp each have different APIs. We provide a single, + unified interface to control them all. +

+
+ +
+
+ {/* Subtle Background Grid */} +
+ + + + {/* Glow effect for active lines */} + + + + + + + {/* Define curved paths with their respective brand colors */} + {(() => { + const curvedPaths = [ + { d: "M480 225 C540 225, 560 90, 620 90", label: "Claude", color: "#d97757" }, + { d: "M480 225 C540 225, 560 180, 620 180", label: "OpenAI", color: "#ffffff" }, + { d: "M480 225 C540 225, 560 270, 620 270", label: "OpenCode", color: "#10B981" }, + { d: "M480 225 C540 225, 560 360, 620 360", label: "Amp", color: "#F59E0B" } + ]; + + return ( + <> + {/* Connection Lines */} + + {/* App -> Agent (Straight) */} + + + {/* Agent -> Providers (Curved) */} + {curvedPaths.map((path, i) => ( + + ))} + + + {/* High-Performance Tracers */} + {/* Blue Tracer: App to SDK */} + + + + + + {/* Colored Tracers: SDK to Providers (following curves and matching brand colors) */} + {curvedPaths.map((path, i) => ( + + + + + ))} + + ); + })()} + + {/* Nodes */} + {/* App Node */} + + + Client App + + + {/* Central SDK Node */} + + + Sandbox Agent SDK + + + {/* Provider Nodes with Logos - Vertical Layout (centered) */} + {/* Claude */} + + + +
+ Claude +
+
+ Claude Code +
+ + {/* Codex */} + + + +
+ + + +
+
+ Codex +
+ + {/* OpenCode */} + + + +
+ + + + +
+
+ OpenCode +
+ + {/* Amp */} + + + +
+ Amp +
+
+ Amp +
+
+
+
+
+ + {/* Server Mode - Span 5 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+ +

Server Mode

+
+

+ Run as an HTTP server anywhere. One command to bridge coding agents to your + application. +

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

Universal Schema

+
+

+ Standardized session schema that covers all features of all agents. Includes tool calls, permission requests, file edits, etc. +

+
+ + {/* Automatic Agent Installation - Span 8 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+
+ +

Automatic Agent Installation

+
+

+ Agents are automatically installed on first use. No manual setup required. +

+
+
+
+ {['Claude Code', 'Codex', 'OpenCode', 'Amp'].map((agent) => ( + + {agent} + + ))} +
+
+
+ + {/* Provider Agnostic - Span 4 cols */} +
+ {/* Top Shine Highlight */} +
+ {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight */} +
+ +
+ +

Provider Agnostic

+
+

+ Run locally, in Docker, or deploy to E2B, Daytona, and Vercel. Same SDK everywhere. +

+
+ {['Local', 'Docker', 'E2B', 'Daytona', 'Vercel', 'Netlify'].map((provider) => ( + + {provider} + + ))} +
+
+
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/Footer.tsx b/frontend/packages/website/src/components/Footer.tsx new file mode 100644 index 0000000..57211ad --- /dev/null +++ b/frontend/packages/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 ( +
+
+
+ {/* Logo & Social */} +
+ Rivet +

+ Build and scale stateful workloads +

+
+ {footer.social.map((item) => ( + + {item.name} + {item.icon} + + ))} +
+
+ + {/* Links */} +
+
+

Products

+ +
+
+

Developers

+ +
+
+

Legal

+ +
+
+
+ + {/* Bottom */} +
+

+ © {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved. +

+
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/GetStarted.tsx b/frontend/packages/website/src/components/GetStarted.tsx new file mode 100644 index 0000000..63c87ef --- /dev/null +++ b/frontend/packages/website/src/components/GetStarted.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { Code, Server, GitBranch } from 'lucide-react'; +import { CopyButton } from './ui/CopyButton'; + +const sdkCodeRaw = `import { SandboxAgent } from "sandbox-agent"; + +const client = await SandboxAgent.start(); + +await client.createSession("my-session", { + agent: "claude-code", +}); + +await client.postMessage("my-session", { + message: "Hello, world!", +}); + +for await (const event of client.streamEvents("my-session")) { + console.log(event.type, event.data); +}`; + +function SdkCodeHighlighted() { + return ( +
+      
+        import
+        {" { "}
+        SandboxAgent
+        {" } "}
+        from
+         
+        "sandbox-agent"
+        ;
+        {"\n\n"}
+        const
+         client = 
+        await
+         SandboxAgent.
+        start
+        ();
+        {"\n\n"}
+        await
+         client.
+        createSession
+        (
+        "my-session"
+        {", {"}
+        {"\n"}
+        {"  agent: "}
+        "claude-code"
+        ,
+        {"\n"}
+        {"});"}
+        {"\n\n"}
+        await
+         client.
+        postMessage
+        (
+        "my-session"
+        {", {"}
+        {"\n"}
+        {"  message: "}
+        "Hello, world!"
+        ,
+        {"\n"}
+        {"});"}
+        {"\n\n"}
+        for await
+         (
+        const
+         event 
+        of
+         client.
+        streamEvents
+        (
+        "my-session"
+        {")) {"}
+        {"\n"}
+        {"  console."}
+        log
+        (event.type, event.data);
+        {"\n"}
+        {"}"}
+      
+    
+ ); +} + +const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh`; + +const sourceCommands = `git clone https://github.com/rivet-dev/sandbox-agent +cd sandbox-agent +cargo run -p sandbox-agent --release`; + +export function GetStarted() { + return ( +
+
+
+

+ Get Started +

+

+ Choose the installation method that works best for your use case. +

+
+ +
+ {/* Option 1: SDK */} +
+
+
+
+ +
+
+ +
+
+

TypeScript SDK

+

Embed in your application

+
+
+ +

+ Import the TypeScript SDK directly into your Node or browser application. Full type safety and streaming support. +

+ +
+
+
+ example.ts + +
+ +
+
+
+ + {/* Option 2: Sandbox */} +
+
+
+
+ +
+
+ +
+
+

HTTP API

+

Run as a server

+
+
+ +

+ Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Vercel, or your own infrastructure. +

+ +
+
+
+ terminal + +
+
+                  
+                    $ 
+                    curl -fsSL \
+                    {"\n"}
+                    {"    "}
+                    https://releases.rivet.dev/sandbox-agent/latest/install.sh
+                     | 
+                    sh
+                  
+                
+
+
+
+ + {/* Option 3: Build from Source */} +
+
+
+
+ +
+
+ +
+
+

Open Source

+

Full control

+
+
+ +

+ Clone the repo and build with Cargo. Customize, contribute, or embed directly in your Rust project. +

+ +
+
+
+ terminal + +
+
+                  
+                    $ 
+                    git clone
+                     
+                    https://github.com/rivet-dev/sandbox-agent
+                    {"\n"}
+                    $ 
+                    cd
+                     sandbox-agent
+                    {"\n"}
+                    $ 
+                    cargo run
+                     -p sandbox-agent --release
+                  
+                
+
+
+
+
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/GitHubStars.tsx b/frontend/packages/website/src/components/GitHubStars.tsx new file mode 100644 index 0000000..38799aa --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/components/Hero.tsx b/frontend/packages/website/src/components/Hero.tsx new file mode 100644 index 0000000..f0691ea --- /dev/null +++ b/frontend/packages/website/src/components/Hero.tsx @@ -0,0 +1,136 @@ +'use client'; + +import { useState } from 'react'; +import { Terminal, Check, ArrowRight } from 'lucide-react'; + +const CopyInstallButton = () => { + const [copied, setCopied] = useState(false); + const installCommand = 'npx skills add https://sandboxagent.dev/docs'; + + 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 ( +
+
+
+
+

+ Universal API for
+ Coding Agents +

+

+ One SDK to control Claude Code, Codex, OpenCode, and Amp. Unified events, session management, and human-in-the-loop. Swap agents with zero refactoring. +

+ + +
+ +
+
+
+
+
+
+
+
+
+
+
example_agent.ts
+
+
+                  
+                    const
+                     agents = 
+                    await
+                     client.
+                    listAgents
+                    ();
+                    {"\n\n"}
+                    await
+                     client.
+                    createSession
+                    (
+                    "demo"
+                    {", {"}
+                    {"\n"}
+                    {"  agent: "}
+                    "codex"
+                    ,
+                    {"\n"}
+                    {"  agentMode: "}
+                    "default"
+                    ,
+                    {"\n"}
+                    {"  permissionMode: "}
+                    "plan"
+                    ,
+                    {"\n"}
+                    {"});"}
+                    {"\n\n"}
+                    await
+                     client.
+                    postMessage
+                    (
+                    "demo"
+                    {", { message: "}
+                    "Hello from the SDK."
+                    {" });"}
+                    {"\n\n"}
+                    for await
+                     (
+                    const
+                     event 
+                    of
+                     client.
+                    streamEvents
+                    (
+                    "demo"
+                    {", { offset: "}
+                    0
+                    {" })) {"}
+                    {"\n"}
+                    {"  console."}
+                    log
+                    (event.type, event.data);
+                    {"\n"}
+                    {"}"}
+                  
+                
+
+
+
+
+
+
+ ); +} + diff --git a/frontend/packages/website/src/components/Integrations.tsx b/frontend/packages/website/src/components/Integrations.tsx new file mode 100644 index 0000000..71bb915 --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/components/Navigation.tsx b/frontend/packages/website/src/components/Navigation.tsx new file mode 100644 index 0000000..2add44f --- /dev/null +++ b/frontend/packages/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 ( + + ); +} + +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/frontend/packages/website/src/components/PainPoints.tsx b/frontend/packages/website/src/components/PainPoints.tsx new file mode 100644 index 0000000..e345a29 --- /dev/null +++ b/frontend/packages/website/src/components/PainPoints.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { motion } from 'framer-motion'; + +const frictions = [ + { + number: '01', + title: 'Fragmented Agent Scaffolds', + description: + 'Claude Code, Codex, OpenCode, and Amp each have proprietary APIs. Swapping agents means rewriting your entire integration.', + 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: + "Whether you're running locally, in Docker, or with E2B, Daytona, and Vercel — you shouldn't need different integration code for each.", + solution: 'One SDK, any environment. Deploy locally or to any cloud provider 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 agent process ends. Debugging and replay become 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 ( +
+
+ +

+ Integrating coding agents is hard. +

+

+ Every agent has its own API, event format, and session model. Swapping agents means + rewriting your entire integration. +

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

{friction.title}

+ + {/* Description */} +

{friction.description}

+ + {/* Solution */} +
+

{friction.solution}

+
+ + {/* Visual */} + {friction.visual} +
+ + ); + })} +
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/ProblemsSolved.tsx b/frontend/packages/website/src/components/ProblemsSolved.tsx new file mode 100644 index 0000000..cf6fc4c --- /dev/null +++ b/frontend/packages/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: 'Claude Code, Codex, OpenCode, and Amp each have different APIs. We provide a single interface to control them all.', + icon: Workflow, + color: 'text-accent', + }, + { + title: 'Universal Transcripts', + desc: 'Every agent has its own event format. Our universal schema normalizes them all — stream, store, and replay with ease.', + icon: Database, + color: 'text-purple-400', + }, + { + title: 'Run Anywhere', + desc: 'Lightweight Rust daemon runs locally or in any environment. One command to bridge coding agents to your system.', + icon: Server, + color: 'text-green-400', + }, +]; + +export function ProblemsSolved() { + return ( +
+
+
+

Why Coding Agent SDK?

+

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

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

{item.title}

+

{item.desc}

+
+ ))} +
+
+
+ ); +} diff --git a/frontend/packages/website/src/components/ui/Badge.tsx b/frontend/packages/website/src/components/ui/Badge.tsx new file mode 100644 index 0000000..9023b7f --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/components/ui/Button.tsx b/frontend/packages/website/src/components/ui/Button.tsx new file mode 100644 index 0000000..4706fdf --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/components/ui/CopyButton.tsx b/frontend/packages/website/src/components/ui/CopyButton.tsx new file mode 100644 index 0000000..a6e5b6a --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/components/ui/FeatureIcon.tsx b/frontend/packages/website/src/components/ui/FeatureIcon.tsx new file mode 100644 index 0000000..e6ed276 --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/src/layouts/Layout.astro b/frontend/packages/website/src/layouts/Layout.astro new file mode 100644 index 0000000..97e3873 --- /dev/null +++ b/frontend/packages/website/src/layouts/Layout.astro @@ -0,0 +1,39 @@ +--- +interface Props { + title: string; + description?: string; +} + +const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, and Amp with unified events and sessions." } = Astro.props; +--- + + + + + + + + + + + + {title} + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/website/src/pages/index.astro b/frontend/packages/website/src/pages/index.astro new file mode 100644 index 0000000..e9fb43b --- /dev/null +++ b/frontend/packages/website/src/pages/index.astro @@ -0,0 +1,22 @@ +--- +import Layout from '../layouts/Layout.astro'; +import { Navigation } from '../components/Navigation'; +import { Hero } from '../components/Hero'; +import { FeatureGrid } from '../components/FeatureGrid'; +import { GetStarted } from '../components/GetStarted'; +import { FAQ } from '../components/FAQ'; +import { Footer } from '../components/Footer'; +--- + + +
+ +
+ + + + +
+
+
diff --git a/frontend/packages/website/src/styles/global.css b/frontend/packages/website/src/styles/global.css new file mode 100644 index 0000000..8dbb6f8 --- /dev/null +++ b/frontend/packages/website/src/styles/global.css @@ -0,0 +1,75 @@ +@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; + } + + /* Firefox scrollbar */ + * { + scrollbar-width: thin; + scrollbar-color: #3f3f46 transparent; + } + + /* Webkit scrollbar */ + ::-webkit-scrollbar { + @apply w-2 h-2; + } + + ::-webkit-scrollbar-track { + @apply bg-transparent; + } + + ::-webkit-scrollbar-thumb { + @apply bg-zinc-700 rounded-full; + } + + ::-webkit-scrollbar-thumb:hover { + @apply bg-zinc-600; + } + + ::-webkit-scrollbar-corner { + @apply bg-transparent; + } + + /* Code block scrollbars */ + pre, code, .overflow-x-auto { + scrollbar-width: thin; + scrollbar-color: #52525b #18181b; + } + + pre::-webkit-scrollbar, + code::-webkit-scrollbar, + .overflow-x-auto::-webkit-scrollbar { + @apply h-2; + } + + pre::-webkit-scrollbar-track, + code::-webkit-scrollbar-track, + .overflow-x-auto::-webkit-scrollbar-track { + @apply bg-zinc-900; + } + + pre::-webkit-scrollbar-thumb, + code::-webkit-scrollbar-thumb, + .overflow-x-auto::-webkit-scrollbar-thumb { + @apply bg-zinc-600 rounded-full; + } +} + +@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/frontend/packages/website/tailwind.config.mjs b/frontend/packages/website/tailwind.config.mjs new file mode 100644 index 0000000..142adbc --- /dev/null +++ b/frontend/packages/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/frontend/packages/website/tsconfig.json b/frontend/packages/website/tsconfig.json new file mode 100644 index 0000000..b7243b9 --- /dev/null +++ b/frontend/packages/website/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} diff --git a/og.png b/og.png new file mode 100644 index 0000000..a3f5d89 Binary files /dev/null and b/og.png differ