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 16bd6161 parent 3c7e2c88 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 @@
11 dist/
22 node_modules/
33 .astro/
4+public/types/
v2/apps/www/package.json+3−1
@@ -7,11 +7,13 @@ · 6 unmodified lines
77 "dev": "astro dev",
88 "build": "astro build",
99 "preview": "astro preview",
10− "typecheck": "astro check"
10+ "typecheck": "astro check",
11+ "generate-types": "bun run scripts/generate-types.ts"
1112 },
1213 "dependencies": {
1314 "@astrojs/react": "^4.2.1",
1415 "@astrojs/tailwind": "^6.0.2",
16+ "@monaco-editor/react": "^4.7.0",
1517 "astro": "^5.10.1",
1618 "react": "^19.1.0",
1719 "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+
11+import { prepareOpenApiSpec } from "../../../../executor/packages/core/src/openapi-prepare";
12+import { buildOpenApiToolsFromPrepared } from "../../../../executor/packages/core/src/openapi/tool-builder";
13+import { buildWorkspaceTypeBundle } from "../../../../executor/packages/core/src/tool-typing/typebundle";
14+import type { OpenApiToolSourceConfig } from "../../../../executor/packages/core/src/tool/source-types";
15+import { mkdirSync } from "fs";
16+import { join, dirname } from "path";
17+
18+interface SourceSpec {
19+ name: string;
20+ label: string;
21+ icon: string;
22+ description: string;
23+ specUrl: string;
24+ baseUrl?: string;
25+}
26+
27+const 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+
56+const OUT_DIR = join(
57+ dirname(new URL(import.meta.url).pathname),
58+ "../public/types",
59+);
60+
61+interface ManifestEntry {
62+ name: string;
63+ label: string;
64+ icon: string;
65+ description: string;
66+ toolCount: number;
67+ fileSizeKb: number;
68+ file: string;
69+}
70+
71+async 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+
125+async 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+
165+main().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 @@
1−import { useState } from "react";
2−
3−interface Source {
4− id: string;
5− name: string;
6− icon: string;
7− toolCount: number;
8− description: string;
9−}
10−
11−const 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−
56−export 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 @@
1−import { useState, useEffect, useRef } from "react";
2−
3−const CODE_EXAMPLE = `// Your agent writes TypeScript — not JSON tool calls
4−const zones = await tools.cloudflare.list_zones({
5− account: { id: env.CF_ACCOUNT_ID },
6−})
7−
8−const zone = zones.result.find(z => z.name === "example.com")
9−
10−// Create a DNS record
11−await 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
20−const deploys = await tools.vercel.listDeployments({
21− teamId: env.VERCEL_TEAM_ID,
22− limit: 5,
23−})
24−
25−return {
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−
35−const 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−
52−export 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 @@
1+import { useState, useEffect, useRef, useCallback } from "react";
2+import Editor, { type OnMount, type Monaco } from "@monaco-editor/react";
3+
4+// ---------------------------------------------------------------------------
5+// Types
6+// ---------------------------------------------------------------------------
7+
8+interface SourceManifestEntry {
9+ name: string;
10+ label: string;
11+ icon: string;
12+ description: string;
13+ toolCount: number;
14+ fileSizeKb: number;
15+ file: string;
16+}
17+
18+interface LoadedSource {
19+ name: string;
20+ dts: string;
21+ disposable?: { dispose(): void };
22+}
23+
24+// ---------------------------------------------------------------------------
25+// Default code shown in the editor
26+// ---------------------------------------------------------------------------
27+
28+const DEFAULT_CODE = `// Select sources above to enable autocomplete for real API types.
29+// Try typing "tools." and see what's available.
30+
31+const zones = await tools.cloudflare.zones.list_zones({})
32+
33+const zone = zones.result?.find(z => z.name === "example.com")
34+
35+if (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+
45+const deploys = await tools.vercel.deployments.list_deployments({
46+ limit: 5,
47+})
48+
49+return {
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+
62+const 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+
68+export 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
22 import Base from "../layouts/Base.astro";
33 import Hero from "../components/Hero.astro";
44 import Section from "../components/Section.astro";
5−import { AddSources } from "../components/AddSources";
5+import { SandboxEditor } from "../components/SandboxEditor";
66 import { SystemPrompt } from "../components/SystemPrompt";
7−import { CodeEditor } from "../components/CodeEditor";
87 import { ExampleQueries } from "../components/ExampleQueries";
98 import { ApprovalFlow } from "../components/ApprovalFlow";
109 import { UsageCards } from "../components/UsageCards";
@@ -33,20 +32,15 @@ import { UsageCards } from "../components/UsageCards"; · 22 unmodified lines
3332 </p>
3433
3534 <!-- ======================================================
36− ADD SOURCES
35+ SANDBOX EDITOR — Sources + Monaco with real types
3736 ====================================================== -->
38− <h2>Add any API as a source</h2>
37+ <h2>Add sources, get real types</h2>
3938 <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.
4140 </p>
4241
43− <div class="my-8 bg-surface-elevated border border-white/[0.08] rounded-lg overflow-hidden">
44− <div class="px-5 py-4 bg-white/[0.02] border-b border-white/[0.06]">
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>
42+ <div class="my-8">
43+ <SandboxEditor client:only="react" />
5044 </div>
5145
5246 <!-- ======================================================
@@ -61,18 +55,6 @@ import { UsageCards } from "../components/UsageCards"; · 8 unmodified lines
6155 <SystemPrompt client:visible />
6256 </div>
6357
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−
7658 <blockquote>
7759 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.
7860 </blockquote>
v2/bun.lock+15−0
@@ -85,6 +85,7 @@ · 84 unmodified lines
8585 "dependencies": {
8686 "@astrojs/react": "^4.2.1",
8787 "@astrojs/tailwind": "^6.0.2",
88+ "@monaco-editor/react": "^4.7.0",
8889 "astro": "^5.10.1",
8990 "react": "^19.1.0",
9091 "react-dom": "^19.1.0",
@@ -531,6 +532,10 @@ · 440 unmodified lines
531532
532533 "@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=="],
533534
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+
534539 "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
535540
536541 "@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
787792
788793 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
789794
795+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
796+
790797 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
791798
792799 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -1109,6 +1116,8 @@ · 316 unmodified lines
11091116
11101117 "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
11111118
1119+ "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
1120+
11121121 "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=="],
11131122
11141123 "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
@@ -1387,6 +1396,8 @@ · 272 unmodified lines
13871396
13881397 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
13891398
1399+ "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="],
1400+
13901401 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
13911402
13921403 "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
14991510
15001511 "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
15011512
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+
15021515 "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
15031516
15041517 "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
@@ -1801,6 +1814,8 @@ · 296 unmodified lines
18011814
18021815 "stat-mode": ["stat-mode@0.3.0", "", {}, "sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng=="],
18031816
1817+ "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
1818+
18041819 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
18051820
18061821 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],