History

feat(www): add Monaco editor with real OpenAPI types

Replace static AddSources/CodeEditor with SandboxEditor that uses Monaco with real TypeScript types generated from OpenAPI specs. - generate-types.ts script fetches Cloudflare/Vercel/GitHub specs and produces .d.ts bundles via the executor core pipeline - Types served as static assets, fetched on demand per source toggle - Full IntelliSense: autocomplete, parameter types, return types - 2,603 Cloudflare + 281 Vercel + 1,079 GitHub typed tool methods

Rhys Sullivan committed 8 months ago

Commit 61312691 parent 3e3bdbd8 files · +524 −320Browse files

Unified · Split

8 files
  1. v2/apps/www/.gitignore +1 −0
  2. v2/apps/www/package.json +3 −1
  3. v2/apps/www/scripts/generate-types.ts +168 −0
  4. v2/apps/www/src/components/AddSources.tsx +0 −147
  5. v2/apps/www/src/components/CodeEditor.tsx +0 −148
  6. v2/apps/www/src/components/SandboxEditor.tsx +331 −0
  7. v2/apps/www/src/pages/index.astro +6 −24
  8. v2/bun.lock +15 −0
v2/apps/www/.gitignore+1−0
@@ -1,3 +1,4 @@
1dist/1dist/
2node_modules/2node_modules/
3.astro/3.astro/
4public/types/
v2/apps/www/package.json+3−1
@@ -7,11 +7,13 @@ · 6 unmodified lines
7 "dev": "astro dev",7 "dev": "astro dev",
8 "build": "astro build",8 "build": "astro build",
9 "preview": "astro preview",9 "preview": "astro preview",
10 "typecheck": "astro check"10 "typecheck": "astro check",
11 "generate-types": "bun run scripts/generate-types.ts"
11 },12 },
12 "dependencies": {13 "dependencies": {
13 "@astrojs/react": "^4.2.1",14 "@astrojs/react": "^4.2.1",
14 "@astrojs/tailwind": "^6.0.2",15 "@astrojs/tailwind": "^6.0.2",
16 "@monaco-editor/react": "^4.7.0",
15 "astro": "^5.10.1",17 "astro": "^5.10.1",
16 "react": "^19.1.0",18 "react": "^19.1.0",
17 "react-dom": "^19.1.0",19 "react-dom": "^19.1.0",
v2/apps/www/scripts/generate-types.ts+168−0
@@ -0,0 +1,168 @@
1/**
2 * Fetches real OpenAPI specs and generates .d.ts type bundles for the Monaco editor.
3 *
4 * Usage: bun run scripts/generate-types.ts
5 *
6 * Outputs:
7 * public/types/<source>.d.ts — full type bundle per source (served as static asset)
8 * public/types/manifest.json — metadata about each source (tool count, file size)
9 */
10
11import { prepareOpenApiSpec } from "../../../../executor/packages/core/src/openapi-prepare";
12import { buildOpenApiToolsFromPrepared } from "../../../../executor/packages/core/src/openapi/tool-builder";
13import { buildWorkspaceTypeBundle } from "../../../../executor/packages/core/src/tool-typing/typebundle";
14import type { OpenApiToolSourceConfig } from "../../../../executor/packages/core/src/tool/source-types";
15import { mkdirSync } from "fs";
16import { join, dirname } from "path";
17
18interface SourceSpec {
19 name: string;
20 label: string;
21 icon: string;
22 description: string;
23 specUrl: string;
24 baseUrl?: string;
25}
26
27const SOURCES: SourceSpec[] = [
28 {
29 name: "cloudflare",
30 label: "Cloudflare",
31 icon: "☁",
32 description: "DNS, Workers, R2, KV, Zones, WAF, and more",
33 specUrl:
34 "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
35 baseUrl: "https://api.cloudflare.com/client/v4",
36 },
37 {
38 name: "vercel",
39 label: "Vercel",
40 icon: "▲",
41 description: "Deployments, Domains, Environment Variables, Projects",
42 specUrl: "https://openapi.vercel.sh/",
43 baseUrl: "https://api.vercel.com",
44 },
45 {
46 name: "github",
47 label: "GitHub",
48 icon: "⬡",
49 description: "Repos, Issues, Pull Requests, Actions, Packages",
50 specUrl:
51 "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
52 baseUrl: "https://api.github.com",
53 },
54];
55
56const OUT_DIR = join(
57 dirname(new URL(import.meta.url).pathname),
58 "../public/types",
59);
60
61interface ManifestEntry {
62 name: string;
63 label: string;
64 icon: string;
65 description: string;
66 toolCount: number;
67 fileSizeKb: number;
68 file: string;
69}
70
71async function processSource(source: SourceSpec) {
72 console.log(`\n${"=".repeat(60)}`);
73 console.log(`Processing: ${source.name}`);
74 console.log(`${"=".repeat(60)}`);
75
76 // Step 1: Prepare the spec
77 console.log(" → Fetching and preparing spec...");
78 let spec: string | Record<string, unknown> = source.specUrl;
79
80 if (!/\.json($|[?#])/i.test(source.specUrl)) {
81 console.log(" → URL doesn't end in .json, fetching manually...");
82 const response = await fetch(source.specUrl);
83 if (!response.ok) {
84 throw new Error(
85 `Failed to fetch spec: ${response.status} ${response.statusText}`,
86 );
87 }
88 spec = (await response.json()) as Record<string, unknown>;
89 }
90
91 const prepared = await prepareOpenApiSpec(spec, source.name, {
92 includeDts: true,
93 profile: "full",
94 });
95
96 console.log(` → DTS status: ${prepared.dtsStatus}`);
97 console.log(` → Servers: ${prepared.servers.join(", ") || "(none)"}`);
98
99 // Step 2: Build tool definitions
100 console.log(" → Building tool definitions...");
101 const sourceConfig: OpenApiToolSourceConfig = {
102 type: "openapi",
103 name: source.name,
104 spec: source.specUrl,
105 baseUrl: source.baseUrl,
106 };
107
108 const tools = buildOpenApiToolsFromPrepared(sourceConfig, prepared);
109 console.log(` → Tools built: ${tools.length}`);
110
111 // Step 3: Build .d.ts type bundle
112 console.log(" → Building type bundle...");
113 const sourceKey = `openapi:${source.name}`;
114 const dtsBundle = buildWorkspaceTypeBundle({
115 tools,
116 openApiDtsBySource: prepared.dts ? { [sourceKey]: prepared.dts } : {},
117 });
118
119 const sizeKb = dtsBundle.length / 1024;
120 console.log(` → Bundle size: ${sizeKb.toFixed(1)} KB`);
121
122 return { toolCount: tools.length, dtsBundle, sizeKb };
123}
124
125async function main() {
126 mkdirSync(OUT_DIR, { recursive: true });
127
128 const manifest: ManifestEntry[] = [];
129
130 for (const source of SOURCES) {
131 try {
132 const result = await processSource(source);
133 const fileName = `${source.name}.d.ts`;
134 const outPath = join(OUT_DIR, fileName);
135 await Bun.write(outPath, result.dtsBundle);
136 console.log(` ✓ Written: ${outPath}`);
137
138 manifest.push({
139 name: source.name,
140 label: source.label,
141 icon: source.icon,
142 description: source.description,
143 toolCount: result.toolCount,
144 fileSizeKb: Math.round(result.sizeKb),
145 file: fileName,
146 });
147 } catch (err) {
148 console.error(` ✗ Failed to process ${source.name}:`, err);
149 }
150 }
151
152 // Write manifest
153 const manifestPath = join(OUT_DIR, "manifest.json");
154 await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
155 console.log(`\n✓ Manifest: ${manifestPath}`);
156 console.log(`\nSummary:`);
157 for (const entry of manifest) {
158 console.log(
159 ` ${entry.label}: ${entry.toolCount} tools, ${entry.fileSizeKb} KB`,
160 );
161 }
162 console.log("\nDone!");
163}
164
165main().catch((err) => {
166 console.error("Fatal error:", err);
167 process.exit(1);
168});
v2/apps/www/src/components/AddSources.tsx+0−147
@@ -1,147 +0,0 @@
1import { useState } from "react";
2
3interface Source {
4 id: string;
5 name: string;
6 icon: string;
7 toolCount: number;
8 description: string;
9}
10
11const SOURCES: Source[] = [
12 {
13 id: "cloudflare",
14 name: "Cloudflare",
15 icon: "☁",
16 toolCount: 1347,
17 description: "DNS, Workers, R2, KV, Zones, WAF, and more",
18 },
19 {
20 id: "vercel",
21 name: "Vercel",
22 icon: "▲",
23 toolCount: 289,
24 description: "Deployments, Domains, Environment Variables, Projects",
25 },
26 {
27 id: "posthog",
28 name: "PostHog",
29 icon: "🦔",
30 toolCount: 156,
31 description: "Events, Funnels, Feature Flags, Session Recordings",
32 },
33 {
34 id: "github",
35 name: "GitHub",
36 icon: "⬡",
37 toolCount: 892,
38 description: "Repos, Issues, Pull Requests, Actions, Packages",
39 },
40 {
41 id: "stripe",
42 name: "Stripe",
43 icon: "◈",
44 toolCount: 614,
45 description: "Charges, Subscriptions, Invoices, Customers, Payouts",
46 },
47 {
48 id: "linear",
49 name: "Linear",
50 icon: "◇",
51 toolCount: 203,
52 description: "Issues, Projects, Cycles, Teams, Labels",
53 },
54];
55
56export function AddSources() {
57 const [selected, setSelected] = useState<Set<string>>(
58 new Set(["cloudflare", "vercel", "posthog"]),
59 );
60
61 const toggle = (id: string) => {
62 setSelected((prev) => {
63 const next = new Set(prev);
64 if (next.has(id)) next.delete(id);
65 else next.add(id);
66 return next;
67 });
68 };
69
70 const totalTools = SOURCES.filter((s) => selected.has(s.id)).reduce(
71 (sum, s) => sum + s.toolCount,
72 0,
73 );
74
75 return (
76 <div>
77 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
78 {SOURCES.map((source) => {
79 const isSelected = selected.has(source.id);
80 return (
81 <button
82 key={source.id}
83 onClick={() => toggle(source.id)}
84 className={`group text-left p-4 rounded-lg border transition-all duration-200 ${
85 isSelected
86 ? "border-accent/40 bg-accent/[0.06]"
87 : "border-white/[0.08] bg-white/[0.02] hover:border-white/20"
88 }`}
89 >
90 <div className="flex items-center justify-between mb-2">
91 <div className="flex items-center gap-2.5">
92 <span className="text-lg">{source.icon}</span>
93 <span className="font-medium text-sm text-[#f5f5f5]">
94 {source.name}
95 </span>
96 </div>
97 <div
98 className={`w-4 h-4 rounded border-2 flex items-center justify-center transition-all ${
99 isSelected
100 ? "border-accent bg-accent"
101 : "border-white/20 bg-transparent"
102 }`}
103 >
104 {isSelected && (
105 <svg
106 className="w-2.5 h-2.5 text-white"
107 fill="none"
108 viewBox="0 0 24 24"
109 stroke="currentColor"
110 strokeWidth={3}
111 >
112 <path
113 strokeLinecap="round"
114 strokeLinejoin="round"
115 d="M5 13l4 4L19 7"
116 />
117 </svg>
118 )}
119 </div>
120 </div>
121 <p className="text-xs text-white/40 leading-relaxed">
122 {source.description}
123 </p>
124 <div className="mt-2 text-[0.65rem] uppercase tracking-widest text-white/25">
125 {source.toolCount.toLocaleString()} tools
126 </div>
127 </button>
128 );
129 })}
130 </div>
131 <div className="mt-6 flex items-center justify-between text-sm">
132 <span className="text-white/40">
133 <span className="text-[#f5f5f5] font-medium">
134 {selected.size}
135 </span>{" "}
136 sources selected
137 </span>
138 <span className="text-white/40">
139 <span className="text-accent font-mono font-medium">
140 {totalTools.toLocaleString()}
141 </span>{" "}
142 tools available
143 </span>
144 </div>
145 </div>
146 );
147}
v2/apps/www/src/components/CodeEditor.tsx+0−148
@@ -1,148 +0,0 @@
1import { useState, useEffect, useRef } from "react";
2
3const CODE_EXAMPLE = `// Your agent writes TypeScript — not JSON tool calls
4const zones = await tools.cloudflare.list_zones({
5 account: { id: env.CF_ACCOUNT_ID },
6})
7
8const zone = zones.result.find(z => z.name === "example.com")
9
10// Create a DNS record
11await tools.cloudflare.create_dns_record({
12 zone_id: zone.id,
13 type: "A",
14 name: "api",
15 content: "192.0.2.1",
16 proxied: true,
17})
18
19// Check deployment status
20const deploys = await tools.vercel.listDeployments({
21 teamId: env.VERCEL_TEAM_ID,
22 limit: 5,
23})
24
25return {
26 zone: zone.name,
27 dnsRecordCreated: true,
28 recentDeploys: deploys.deployments.map(d => ({
29 url: d.url,
30 state: d.readyState,
31 created: d.created,
32 })),
33}`;
34
35const OUTPUT = `{
36 "zone": "example.com",
37 "dnsRecordCreated": true,
38 "recentDeploys": [
39 {
40 "url": "my-app-abc123.vercel.app",
41 "state": "READY",
42 "created": 1703044800000
43 },
44 {
45 "url": "my-app-def456.vercel.app",
46 "state": "READY",
47 "created": 1703041200000
48 }
49 ]
50}`;
51
52export function CodeEditor() {
53 const [isRunning, setIsRunning] = useState(false);
54 const [showOutput, setShowOutput] = useState(false);
55 const outputRef = useRef<HTMLDivElement>(null);
56
57 const handleRun = () => {
58 setIsRunning(true);
59 setShowOutput(false);
60 setTimeout(() => {
61 setIsRunning(false);
62 setShowOutput(true);
63 }, 1500);
64 };
65
66 useEffect(() => {
67 if (showOutput && outputRef.current) {
68 outputRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
69 }
70 }, [showOutput]);
71
72 return (
73 <div>
74 <div className="bg-surface border border-white/[0.06] rounded-lg overflow-hidden">
75 {/* Editor header */}
76 <div className="px-4 py-3 bg-white/[0.02] border-b border-white/[0.06] flex items-center justify-between">
77 <div className="flex items-center gap-3">
78 <span className="text-[0.65rem] uppercase tracking-widest text-white/30">
79 execute.ts
80 </span>
81 <span className="text-[0.6rem] px-1.5 py-0.5 rounded bg-white/[0.06] text-white/30">
82 TypeScript
83 </span>
84 </div>
85 <button
86 onClick={handleRun}
87 disabled={isRunning}
88 className={`px-3 py-1.5 text-xs font-medium rounded transition-all ${
89 isRunning
90 ? "bg-white/10 text-white/40 cursor-wait"
91 : "bg-[#f5f5f5] text-surface hover:bg-white/90 active:scale-[0.98]"
92 }`}
93 >
94 {isRunning ? (
95 <span className="flex items-center gap-1.5">
96 <span className="w-3 h-3 border-2 border-white/30 border-t-white/80 rounded-full animate-spin" />
97 Running...
98 </span>
99 ) : (
100 "▶ Run"
101 )}
102 </button>
103 </div>
104 {/* Code body */}
105 <div className="p-4 overflow-x-auto">
106 <pre className="!m-0 !p-0 !bg-transparent !border-none">
107 <code className="!bg-transparent !p-0 font-mono text-[0.8rem] leading-7 text-white/70">
108 {CODE_EXAMPLE}
109 </code>
110 </pre>
111 </div>
112 </div>
113 {/* Output */}
114 {(isRunning || showOutput) && (
115 <div ref={outputRef} className="mt-3 bg-surface border border-white/[0.06] rounded-lg overflow-hidden animate-log-fade-in">
116 <div className="px-4 py-3 bg-white/[0.02] border-b border-white/[0.06] flex items-center gap-2">
117 <span className="text-[0.65rem] uppercase tracking-widest text-white/30">
118 output
119 </span>
120 {isRunning && (
121 <span className="w-3 h-3 border-2 border-white/20 border-t-green-400 rounded-full animate-spin" />
122 )}
123 {showOutput && (
124 <span className="text-[0.65rem] text-green-400">
125 ✓ completed in 1.2s
126 </span>
127 )}
128 </div>
129 <div className="p-4 overflow-x-auto">
130 {isRunning ? (
131 <div className="font-mono text-xs text-white/30 space-y-2">
132 <div className="animate-pulse">→ Resolving tool: cloudflare.list_zones...</div>
133 <div className="animate-pulse" style={{ animationDelay: "0.3s" }}>→ Policy check: allow (matched role "Infrastructure Admin")</div>
134 <div className="animate-pulse" style={{ animationDelay: "0.6s" }}>→ Executing with credentials: cf_api_token_****</div>
135 </div>
136 ) : (
137 <pre className="!m-0 !p-0 !bg-transparent !border-none">
138 <code className="!bg-transparent !p-0 font-mono text-[0.8rem] leading-7 text-green-400/70">
139 {OUTPUT}
140 </code>
141 </pre>
142 )}
143 </div>
144 </div>
145 )}
146 </div>
147 );
148}
v2/apps/www/src/components/SandboxEditor.tsx+331−0
@@ -0,0 +1,331 @@
1import { useState, useEffect, useRef, useCallback } from "react";
2import Editor, { type OnMount, type Monaco } from "@monaco-editor/react";
3
4// ---------------------------------------------------------------------------
5// Types
6// ---------------------------------------------------------------------------
7
8interface SourceManifestEntry {
9 name: string;
10 label: string;
11 icon: string;
12 description: string;
13 toolCount: number;
14 fileSizeKb: number;
15 file: string;
16}
17
18interface LoadedSource {
19 name: string;
20 dts: string;
21 disposable?: { dispose(): void };
22}
23
24// ---------------------------------------------------------------------------
25// Default code shown in the editor
26// ---------------------------------------------------------------------------
27
28const DEFAULT_CODE = `// Select sources above to enable autocomplete for real API types.
29// Try typing "tools." and see what's available.
30
31const zones = await tools.cloudflare.zones.list_zones({})
32
33const zone = zones.result?.find(z => z.name === "example.com")
34
35if (zone) {
36 await tools.cloudflare.dns.create_dns_record({
37 zone_id: zone.id,
38 type: "A",
39 name: "api",
40 content: "192.0.2.1",
41 proxied: true,
42 })
43}
44
45const deploys = await tools.vercel.deployments.list_deployments({
46 limit: 5,
47})
48
49return {
50 zone: zone?.name,
51 recentDeploys: deploys.deployments?.map(d => ({
52 url: d.url,
53 state: d.readyState,
54 })),
55}
56`;
57
58// ---------------------------------------------------------------------------
59// Component
60// ---------------------------------------------------------------------------
61
62const FALLBACK_MANIFEST: SourceManifestEntry[] = [
63 { name: "cloudflare", label: "Cloudflare", icon: "☁", description: "DNS, Workers, R2, KV, Zones, WAF", toolCount: 2603, fileSizeKb: 11196, file: "cloudflare.d.ts" },
64 { name: "vercel", label: "Vercel", icon: "▲", description: "Deployments, Domains, Projects", toolCount: 281, fileSizeKb: 2511, file: "vercel.d.ts" },
65 { name: "github", label: "GitHub", icon: "⬡", description: "Repos, Issues, Pull Requests, Actions", toolCount: 1079, fileSizeKb: 6301, file: "github.d.ts" },
66];
67
68export function SandboxEditor() {
69 const [manifest, setManifest] = useState<SourceManifestEntry[]>(FALLBACK_MANIFEST);
70 const [selected, setSelected] = useState<Set<string>>(new Set(["cloudflare", "vercel", "github"]));
71 const [loading, setLoading] = useState<Set<string>>(new Set());
72 const [loaded, setLoaded] = useState<Map<string, LoadedSource>>(new Map());
73 const monacoRef = useRef<Monaco | null>(null);
74 const editorRef = useRef<Parameters<OnMount>[0] | null>(null);
75
76 // Fetch manifest on mount (overrides fallback with real data if available)
77 useEffect(() => {
78 fetch("/types/manifest.json")
79 .then((r) => {
80 if (!r.ok) throw new Error(`${r.status}`);
81 return r.json();
82 })
83 .then((data: SourceManifestEntry[]) => setManifest(data))
84 .catch(() => {
85 // Keep fallback manifest — editor still shows, just no types loaded
86 });
87 }, []);
88
89 // Load/unload type definitions when selection changes
90 useEffect(() => {
91 const monaco = monacoRef.current;
92 if (!monaco) return;
93
94 // Load newly selected sources
95 for (const name of selected) {
96 if (loaded.has(name) || loading.has(name)) continue;
97
98 const entry = manifest.find((e) => e.name === name);
99 if (!entry) continue;
100
101 setLoading((prev) => new Set(prev).add(name));
102
103 fetch(`/types/${entry.file}`)
104 .then((r) => r.text())
105 .then((dts) => {
106 const uri = `file:///types/${name}.d.ts`;
107 const disposable =
108 monaco.languages.typescript.typescriptDefaults.addExtraLib(
109 dts,
110 uri,
111 );
112
113 setLoaded((prev) => {
114 const next = new Map(prev);
115 next.set(name, { name, dts, disposable });
116 return next;
117 });
118 setLoading((prev) => {
119 const next = new Set(prev);
120 next.delete(name);
121 return next;
122 });
123 })
124 .catch((err) => {
125 console.error(`Failed to load types for ${name}:`, err);
126 setLoading((prev) => {
127 const next = new Set(prev);
128 next.delete(name);
129 return next;
130 });
131 });
132 }
133
134 // Dispose deselected sources
135 for (const [name, source] of loaded) {
136 if (!selected.has(name)) {
137 source.disposable?.dispose();
138 setLoaded((prev) => {
139 const next = new Map(prev);
140 next.delete(name);
141 return next;
142 });
143 }
144 }
145 }, [selected, manifest, loaded, loading]);
146
147 const toggle = useCallback((name: string) => {
148 setSelected((prev) => {
149 const next = new Set(prev);
150 if (next.has(name)) next.delete(name);
151 else next.add(name);
152 return next;
153 });
154 }, []);
155
156 const handleMount: OnMount = (editor, monaco) => {
157 editorRef.current = editor;
158 monacoRef.current = monaco;
159
160 // Configure TypeScript compiler
161 monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
162 target: monaco.languages.typescript.ScriptTarget.ESNext,
163 module: monaco.languages.typescript.ModuleKind.ESNext,
164 moduleResolution:
165 monaco.languages.typescript.ModuleResolutionKind.NodeJs,
166 allowNonTsExtensions: true,
167 strict: true,
168 noEmit: true,
169 esModuleInterop: true,
170 lib: ["esnext"],
171 });
172
173 // Disable built-in lib fetching — we provide our own types
174 monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
175 monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
176 noSemanticValidation: false,
177 noSyntaxValidation: false,
178 });
179 };
180
181 const totalTools = manifest
182 .filter((s) => selected.has(s.name))
183 .reduce((sum, s) => sum + s.toolCount, 0);
184
185 return (
186 <div>
187 {/* Source toggles */}
188 <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
189 {manifest.map((source) => {
190 const isSelected = selected.has(source.name);
191 const isLoading = loading.has(source.name);
192 const isLoaded = loaded.has(source.name);
193
194 return (
195 <button
196 key={source.name}
197 onClick={() => toggle(source.name)}
198 className={`group text-left p-3 rounded-lg border transition-all duration-200 ${
199 isSelected
200 ? "border-accent/40 bg-accent/[0.06]"
201 : "border-white/[0.08] bg-white/[0.02] hover:border-white/20"
202 }`}
203 >
204 <div className="flex items-center justify-between">
205 <div className="flex items-center gap-2">
206 <span className="text-base">{source.icon}</span>
207 <span className="font-medium text-sm text-[#f5f5f5]">
208 {source.label}
209 </span>
210 {isLoading && (
211 <span className="w-3 h-3 border-2 border-white/20 border-t-accent rounded-full animate-spin" />
212 )}
213 {isSelected && isLoaded && (
214 <span className="text-[0.6rem] text-green-400/60">
215 ✓
216 </span>
217 )}
218 </div>
219 <div
220 className={`w-4 h-4 rounded border-2 flex items-center justify-center transition-all ${
221 isSelected
222 ? "border-accent bg-accent"
223 : "border-white/20 bg-transparent"
224 }`}
225 >
226 {isSelected && (
227 <svg
228 className="w-2.5 h-2.5 text-white"
229 fill="none"
230 viewBox="0 0 24 24"
231 stroke="currentColor"
232 strokeWidth={3}
233 >
234 <path
235 strokeLinecap="round"
236 strokeLinejoin="round"
237 d="M5 13l4 4L19 7"
238 />
239 </svg>
240 )}
241 </div>
242 </div>
243 <div className="mt-1.5 flex items-center justify-between">
244 <span className="text-xs text-white/40">
245 {source.description}
246 </span>
247 </div>
248 <div className="mt-1 text-[0.6rem] uppercase tracking-widest text-white/25">
249 {source.toolCount.toLocaleString()} tools
250 </div>
251 </button>
252 );
253 })}
254 </div>
255
256 {/* Stats bar */}
257 <div className="flex items-center justify-between text-xs text-white/40 mb-3 px-1">
258 <span>
259 <span className="text-[#f5f5f5] font-medium">{selected.size}</span>{" "}
260 source{selected.size !== 1 ? "s" : ""} selected
261 </span>
262 <span>
263 <span className="text-accent font-mono font-medium">
264 {totalTools.toLocaleString()}
265 </span>{" "}
266 typed tools available
267 </span>
268 </div>
269
270 {/* Monaco Editor */}
271 <div className="bg-surface border border-white/[0.06] rounded-lg overflow-hidden">
272 <div className="px-4 py-3 bg-white/[0.02] border-b border-white/[0.06] flex items-center justify-between">
273 <div className="flex items-center gap-3">
274 <span className="text-[0.65rem] uppercase tracking-widest text-white/30">
275 sandbox.ts
276 </span>
277 <span className="text-[0.6rem] px-1.5 py-0.5 rounded bg-white/[0.06] text-white/30">
278 TypeScript
279 </span>
280 </div>
281 <div className="flex items-center gap-2 text-[0.6rem] text-white/25">
282 {loading.size > 0 && (
283 <span className="flex items-center gap-1.5">
284 <span className="w-2.5 h-2.5 border-2 border-white/20 border-t-accent rounded-full animate-spin" />
285 Loading types...
286 </span>
287 )}
288 {loading.size === 0 && loaded.size > 0 && (
289 <span className="text-green-400/50">Types loaded</span>
290 )}
291 </div>
292 </div>
293 <Editor
294 height="420px"
295 defaultLanguage="typescript"
296 defaultValue={DEFAULT_CODE}
297 theme="vs-dark"
298 onMount={handleMount}
299 options={{
300 minimap: { enabled: false },
301 fontSize: 13,
302 lineHeight: 24,
303 padding: { top: 16, bottom: 16 },
304 scrollBeyondLastLine: false,
305 wordWrap: "on",
306 tabSize: 2,
307 automaticLayout: true,
308 suggestOnTriggerCharacters: true,
309 quickSuggestions: true,
310 renderLineHighlight: "none",
311 overviewRulerBorder: false,
312 hideCursorInOverviewRuler: true,
313 scrollbar: {
314 verticalScrollbarSize: 6,
315 horizontalScrollbarSize: 6,
316 },
317 fontFamily: "'JetBrains Mono', monospace",
318 fontLigatures: true,
319 }}
320 />
321 </div>
322
323 {/* Hint */}
324 <div className="mt-4 px-4 py-3 bg-blue-500/[0.05] border-l-2 border-blue-400 text-sm text-white/60">
325 These are <span className="text-[#f5f5f5] font-medium">real types</span> generated
326 from the actual OpenAPI specs — {totalTools.toLocaleString()} typed tool methods across {selected.size} sources.
327 Try typing <code className="text-accent">tools.</code> and using autocomplete.
328 </div>
329 </div>
330 );
331}
v2/apps/www/src/pages/index.astro+6−24
@@ -2,9 +2,8 @@ · 1 unmodified line
2import Base from "../layouts/Base.astro";2import Base from "../layouts/Base.astro";
3import Hero from "../components/Hero.astro";3import Hero from "../components/Hero.astro";
4import Section from "../components/Section.astro";4import Section from "../components/Section.astro";
5import { AddSources } from "../components/AddSources";5import { SandboxEditor } from "../components/SandboxEditor";
6import { SystemPrompt } from "../components/SystemPrompt";6import { SystemPrompt } from "../components/SystemPrompt";
7import { CodeEditor } from "../components/CodeEditor";
8import { ExampleQueries } from "../components/ExampleQueries";7import { ExampleQueries } from "../components/ExampleQueries";
9import { ApprovalFlow } from "../components/ApprovalFlow";8import { ApprovalFlow } from "../components/ApprovalFlow";
10import { UsageCards } from "../components/UsageCards";9import { UsageCards } from "../components/UsageCards";
@@ -33,20 +32,15 @@ import { UsageCards } from "../components/UsageCards"; · 22 unmodified lines
33 </p>32 </p>
34 33
35 <!-- ======================================================34 <!-- ======================================================
36 ADD SOURCES35 SANDBOX EDITOR — Sources + Monaco with real types
37 ====================================================== -->36 ====================================================== -->
38 <h2>Add any API as a source</h2>37 <h2>Add sources, get real types</h2>
39 <p>38 <p>
40 Connect OpenAPI specs, MCP servers, or GraphQL endpoints. Each source is compiled into typed, discoverable tools — not raw schema definitions pasted into context.39 Connect OpenAPI specs, MCP servers, or GraphQL endpoints. Each source is compiled into a full TypeScript type bundle — real types from the real spec, loaded into a real editor. Toggle sources and see the types update live.
41 </p>40 </p>
42 41
43 <div class="my-8 bg-surface-elevated border border-white/[0.08] rounded-lg overflow-hidden">42 <div class="my-8">
44 <div class="px-5 py-4 bg-white/[0.02] border-b border-white/[0.06]">43 <SandboxEditor client:only="react" />
45 <h4 class="font-serif text-lg font-normal m-0">Add sources</h4>
46 </div>
47 <div class="p-5">
48 <AddSources client:visible />
49 </div>
50 </div>44 </div>
51 45
52 <!-- ======================================================46 <!-- ======================================================
@@ -61,18 +55,6 @@ import { UsageCards } from "../components/UsageCards"; · 8 unmodified lines
61 <SystemPrompt client:visible />55 <SystemPrompt client:visible />
62 </div>56 </div>
63 57
64 <!-- ======================================================
65 CODE EDITOR
66 ====================================================== -->
67 <h2>Your agent writes code, not JSON</h2>
68 <p>
69 Instead of rigid JSON tool calls that fail on the first typo, your agent writes TypeScript in a sandboxed runtime. It gets autocomplete-style discovery, can chain multiple operations, handle errors, and return structured results — all in a single execution.
70 </p>
71
72 <div class="my-8">
73 <CodeEditor client:visible />
74 </div>
75
76 <blockquote>58 <blockquote>
77 The sandbox has no filesystem, no process access, no imports. The only way to reach the outside world is through the <code>tools</code> object — and every call goes through your policies.59 The sandbox has no filesystem, no process access, no imports. The only way to reach the outside world is through the <code>tools</code> object — and every call goes through your policies.
78 </blockquote>60 </blockquote>
v2/bun.lock+15−0
@@ -85,6 +85,7 @@ · 84 unmodified lines
85 "dependencies": {85 "dependencies": {
86 "@astrojs/react": "^4.2.1",86 "@astrojs/react": "^4.2.1",
87 "@astrojs/tailwind": "^6.0.2",87 "@astrojs/tailwind": "^6.0.2",
88 "@monaco-editor/react": "^4.7.0",
88 "astro": "^5.10.1",89 "astro": "^5.10.1",
89 "react": "^19.1.0",90 "react": "^19.1.0",
90 "react-dom": "^19.1.0",91 "react-dom": "^19.1.0",
@@ -531,6 +532,10 @@ · 440 unmodified lines
531 532
532 "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],533 "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
533 534
535 "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="],
536
537 "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="],
538
534 "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],539 "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
535 540
536 "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],541 "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
@@ -787,6 +792,8 @@ · 250 unmodified lines
787 792
788 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],793 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
789 794
795 "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
796
790 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],797 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
791 798
792 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],799 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -1109,6 +1116,8 @@ · 316 unmodified lines
1109 1116
1110 "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],1117 "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
1111 1118
1119 "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
1120
1112 "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],1121 "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
1113 1122
1114 "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],1123 "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
@@ -1387,6 +1396,8 @@ · 272 unmodified lines
1387 1396
1388 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],1397 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
1389 1398
1399 "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="],
1400
1390 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],1401 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
1391 1402
1392 "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],1403 "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
@@ -1499,6 +1510,8 @@ · 106 unmodified lines
1499 1510
1500 "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],1511 "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
1501 1512
1513 "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="],
1514
1502 "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],1515 "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
1503 1516
1504 "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],1517 "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
@@ -1801,6 +1814,8 @@ · 296 unmodified lines
1801 1814
1802 "stat-mode": ["stat-mode@0.3.0", "", {}, "sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng=="],1815 "stat-mode": ["stat-mode@0.3.0", "", {}, "sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng=="],
1803 1816
1817 "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
1818
1804 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],1819 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1805 1820
1806 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],1821 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],