From 06d34c6df66db8881f50c77a8d0c27e4a870ed27 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:47:58 -0700 Subject: [PATCH 01/37] marketing: homepage variants for mission, proof, captions, and agent markdown Co-Authored-By: Claude Fable 5.1 --- apps/marketing/public/llms.txt | 8 + .../components/variants/agents-footer.astro | 33 ++ .../src/components/variants/mission-a.astro | 100 +++++ .../src/components/variants/mission-b.astro | 106 +++++ .../src/components/variants/mission-c.astro | 122 ++++++ .../src/components/variants/proof-a.astro | 228 ++++++++++ .../src/components/variants/proof-b.astro | 232 +++++++++++ .../src/components/variants/proof-c.astro | 121 ++++++ apps/marketing/src/content/site-copy.ts | 183 ++++++++ apps/marketing/src/pages/index.astro | 168 ++++++-- apps/marketing/src/pages/index.md.ts | 144 +++++++ apps/marketing/src/pages/pricing.md.ts | 41 ++ apps/marketing/src/pages/setup-prompt.md.ts | 11 + apps/marketing/src/styles/global.css | 20 + apps/marketing/src/styles/variants/agents.css | 91 ++++ .../marketing/src/styles/variants/mission.css | 257 ++++++++++++ apps/marketing/src/styles/variants/proof.css | 389 ++++++++++++++++++ 17 files changed, 2217 insertions(+), 37 deletions(-) create mode 100644 apps/marketing/src/components/variants/agents-footer.astro create mode 100644 apps/marketing/src/components/variants/mission-a.astro create mode 100644 apps/marketing/src/components/variants/mission-b.astro create mode 100644 apps/marketing/src/components/variants/mission-c.astro create mode 100644 apps/marketing/src/components/variants/proof-a.astro create mode 100644 apps/marketing/src/components/variants/proof-b.astro create mode 100644 apps/marketing/src/components/variants/proof-c.astro create mode 100644 apps/marketing/src/content/site-copy.ts create mode 100644 apps/marketing/src/pages/index.md.ts create mode 100644 apps/marketing/src/pages/pricing.md.ts create mode 100644 apps/marketing/src/pages/setup-prompt.md.ts create mode 100644 apps/marketing/src/styles/variants/agents.css create mode 100644 apps/marketing/src/styles/variants/mission.css create mode 100644 apps/marketing/src/styles/variants/proof.css diff --git a/apps/marketing/public/llms.txt b/apps/marketing/public/llms.txt index d98199e407..8b4cbab053 100644 --- a/apps/marketing/public/llms.txt +++ b/apps/marketing/public/llms.txt @@ -23,6 +23,14 @@ Run it your way: local CLI, a desktop app, hosted Executor Cloud, or self-hosted - [Pricing](https://executor.sh/#pricing): plans for Executor Cloud. - [Install](https://executor.sh/#install): install the CLI and connect your first agent. +## For agents + +Markdown representations of the pages above, for machines rather than browsers. + +- [/index.md](https://executor.sh/index.md): the homepage as Markdown — what Executor is, how it works, what you get, ways to run it, pricing, and FAQ. +- [/setup-prompt.md](https://executor.sh/setup-prompt.md): a prompt to paste into a coding agent. It picks the right form of Executor, installs it, connects over MCP, and gets a first connection working. +- [/pricing.md](https://executor.sh/pricing.md): the Executor Cloud plans as Markdown. + ## Source - [GitHub](https://github.com/UsefulSoftwareCo/executor): source for the integration layer, plugins, and hosts. diff --git a/apps/marketing/src/components/variants/agents-footer.astro b/apps/marketing/src/components/variants/agents-footer.astro new file mode 100644 index 0000000000..32a3977f4f --- /dev/null +++ b/apps/marketing/src/components/variants/agents-footer.astro @@ -0,0 +1,33 @@ +--- +// Footer fragment: "For humans / For agents". +// +// The HTML homepage is the humans side (active, current page); the agents side +// links to /index.md, the Markdown representation. Below it, a mono line lists +// every machine-readable surface so an agent scraping the footer finds them +// all in one place. +// +// Grayscale only, existing tokens. Styles live in +// src/styles/variants/agents.css, prefixed `.agents-`. +import "../../styles/variants/agents.css"; + +const MARKDOWN_PATHS = ["/index.md", "/setup-prompt.md", "/pricing.md", "/llms.txt"]; +--- + + diff --git a/apps/marketing/src/components/variants/mission-a.astro b/apps/marketing/src/components/variants/mission-a.astro new file mode 100644 index 0000000000..a31c95751a --- /dev/null +++ b/apps/marketing/src/components/variants/mission-a.astro @@ -0,0 +1,100 @@ +--- +// Mission, variant A: "Thesis". +// Three short paragraphs of prose with inline reference markers, and a +// numbered reference list at the foot of the section. Replaces the old +// one-sentence wedge. +import "../../styles/variants/mission.css"; +--- + +
+
+
Mission
+ +
+

+ Every agent client re-implements every integration. The same API gets wired up again for each new client, with its own auth, + its own schema handling, and its own failure modes. Credentials end up pasted + in five places. Nothing shares an idea of what a tool is allowed to do, so + each client guesses.1 +

+ +

+ Executor gives every tool one shape. A name, an input schema, an output + schema. It does not matter whether the tool came from an MCP server, an + OpenAPI spec, or a GraphQL API. Any agent calls any of them the same + way. Tools should belong to you, not to one client or one model + provider.2 +

+ +

+ The safe path has to be the easy path. Executor keeps the semantics it + imported: GET against DELETE for OpenAPI, destructiveHint for MCP, + mutations for GraphQL.3 Calls run in a sandbox. Credentials are attached host-side at call time, + so a raw token never reaches the sandbox, the agent, or the model.4 +

+
+ +
+
References
+
    +
  1. + [1] + Code execution with MCP Anthropic, 2025 +
  2. +
  3. + [2] + Code Mode Cloudflare +
  4. +
  5. + [3] + Tools, tool annotations Model Context Protocol, 2025-06-18 +
  6. +
  7. + [4] + Sandboxing and credentials + Executor docs +
  8. +
+
+
+
diff --git a/apps/marketing/src/components/variants/mission-b.astro b/apps/marketing/src/components/variants/mission-b.astro new file mode 100644 index 0000000000..1c5397940c --- /dev/null +++ b/apps/marketing/src/components/variants/mission-b.astro @@ -0,0 +1,106 @@ +--- +// Mission, variant B: "Numbered claims". +// A short headline, then three hairline-separated rows. Each row is a mono +// index, a bold one-sentence claim, and two sentences of support. +import "../../styles/variants/mission.css"; +--- + +
+
+
Mission
+

+ Three things we think are + true. +

+ +
+
+
001
+
+

Tools should outlive the client.

+

+ Today every agent client re-implements every integration, and the + same credentials get pasted in five places. Your tools should belong + to you, not to one client or one model provider.1 +

+
+
+ +
+
002
+
+

One shape for every tool.

+

+ An MCP server, an OpenAPI spec, and a GraphQL API all become a name, + an input schema, and an output schema. Any agent then calls any of + them the same way, on any surface.2 +

+
+
+ +
+
003
+
+

The safe path is the easy path.

+

+ Executor keeps the semantics it imported: GET against DELETE, + destructiveHint, GraphQL mutations.3 Calls run in a sandbox, and credentials are attached host-side at call + time, so a raw token never reaches the model.4 +

+
+
+
+ + +
+
diff --git a/apps/marketing/src/components/variants/mission-c.astro b/apps/marketing/src/components/variants/mission-c.astro new file mode 100644 index 0000000000..a0de36c737 --- /dev/null +++ b/apps/marketing/src/components/variants/mission-c.astro @@ -0,0 +1,122 @@ +--- +// Mission, variant C: "Two column with pull quotes". +// Tightened thesis prose on the left, two margin notes on the right. The +// notes column sits beside the prose from 900px up and stacks beneath it +// below that. Both quotes are verbatim from the linked sources. +import "../../styles/variants/mission.css"; +--- + +
+
+
Mission
+ +
+
+

+ Every agent client re-implements every integration. The same API is wired up again for each client, with its own auth and + its own failure modes. Credentials get pasted in five places. No client + shares an idea of what a tool may do.1 +

+ +

+ Executor gives every tool one shape. A name, an input schema, an + output schema. MCP, OpenAPI, or GraphQL, it makes no difference: any + agent calls any of them the same way. Tools should belong to you, not + to one client or one model provider.2 +

+ +

+ The safe path has to be the easy path. Executor keeps the semantics it + imported: GET against DELETE, destructiveHint, GraphQL mutations.3 Calls run in a sandbox. Credentials are attached host-side, so a raw token + never reaches the model.4 +

+
+ + +
+ + +
+
diff --git a/apps/marketing/src/components/variants/proof-a.astro b/apps/marketing/src/components/variants/proof-a.astro new file mode 100644 index 0000000000..00bba9cca7 --- /dev/null +++ b/apps/marketing/src/components/variants/proof-a.astro @@ -0,0 +1,228 @@ +--- +/** + * Proof section, variant A: "Tile grid". + * + * A 1px-gap grid of evidence tiles. Every tile is one real, checkable number + * with a mono caption, its source, a big tabular numeral, and a small + * grayscale visual. Nothing here is illustrative: each value links out to + * where it came from, or to the demo that produces it on the page. + */ +import "../../styles/variants/proof.css"; + +interface Props { + readonly stars: string | null; +} + +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; + +// Context figures come from the interactive demo on this page, with GitHub, +// Stripe, Jira and Sentry connected. +const NAIVE_TOKENS = 278_800; +const EXEC_TOKENS = 1_044; +const NAIVE_TOOLS = 1_640; +const EXEC_TOOLS = 1; + +// The two pairs span three orders of magnitude, so the tile bars use a log +// scale (labelled as such) to stay readable. The hero variant shows the same +// pair linearly. +const logWidth = (value: number, max: number): string => + `${Math.max(2, (Math.log10(value + 1) / Math.log10(max + 1)) * 100).toFixed(1)}%`; + +const fmt = (n: number): string => n.toLocaleString("en-US"); +--- + +
+
+
+
In practice
+

Numbers you can check.

+

+ No benchmark we ran on ourselves in private. Each figure below links to + the place it comes from: the demo on this page, the repo, or the + registry. +

+
+ +
+ {/* Context tokens: the headline pair, on a log scale */} +
+
Context tokens
+
Four integrations connected
+
+ {fmt(EXEC_TOKENS)}tok +
+
+
+
+ Naive + + {fmt(NAIVE_TOKENS)} +
+
+ Executor + + {fmt(EXEC_TOKENS)} +
+
+
+
+ Log scale + Source ↗ +
+
+ + {/* Tools exposed to the model */} +
+
Tools exposed
+
Measured in the demo below
+
+ {EXEC_TOOLS}tool +
+
+
+
+ Naive + + {fmt(NAIVE_TOOLS)} +
+
+ Executor + + {fmt(EXEC_TOOLS)} +
+
+
+
+ Log scale + Source ↗ +
+
+ + {/* GitHub stars: only rendered when the page fetch succeeded */} + { + stars != null && ( +
+
GitHub stars
+
UsefulSoftwareCo / executor
+
{stars}
+
+ +
+
+ 305 forks + + Source ↗ + +
+
+ ) + } + + {/* npm downloads */} +
+
npm downloads
+
Package: executor, last 30 days
+
+ 70k/ 30d +
+
+ +
+
+ MIT licensed + Source ↗ +
+
+ + {/* Free tier */} +
+
Free tier
+
Executions per month
+
100,000
+
+ +
+
+ 1 square = 10,000 + Source ↗ +
+
+ + {/* Protocols */} +
+
Protocols supported
+
MCP, OpenAPI, GraphQL, Discovery
+
4
+
+ +
+
+ More on request + Source ↗ +
+
+
+
+
diff --git a/apps/marketing/src/components/variants/proof-b.astro b/apps/marketing/src/components/variants/proof-b.astro new file mode 100644 index 0000000000..032a6e50b8 --- /dev/null +++ b/apps/marketing/src/components/variants/proof-b.astro @@ -0,0 +1,232 @@ +--- +/** + * Proof section, variant B: "Ledger". + * + * The same evidence as variant A, but set as one hairline-ruled spec sheet: + * caption, a thin proportional bar where there is a before/after, the value in + * right-aligned tabular numerals, and the source. It reads like a data sheet + * rather than a set of cards. + * + * Deliberately absent: an "independent benchmark" card. We do not have a + * third-party benchmark yet, so there is no social-proof slot to fill. The + * closing line points at the sources instead. + */ +import "../../styles/variants/proof.css"; + +interface Props { + readonly stars: string | null; +} + +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; + +const NAIVE_TOKENS = 278_800; +const EXEC_TOKENS = 1_044; +const NAIVE_TOOLS = 1_640; +const EXEC_TOOLS = 1; + +// Three orders of magnitude between the pairs, so the row bars use a log +// scale. Each such row says so in its caption. +const logWidth = (value: number, max: number): string => + `${Math.max(2, (Math.log10(value + 1) / Math.log10(max + 1)) * 100).toFixed(1)}%`; + +const fmt = (n: number): string => n.toLocaleString("en-US"); +--- + +
+
+
+
In practice
+

Numbers you can check.

+

+ The whole record, in one sheet. Two of these you can reproduce yourself + in the demo on this page. The rest are public counters. +

+
+ +
+ {/* Context tokens */} +
+
+ Context tokens + Four integrations, log scale +
+
+
+
+ Naive + + {fmt(NAIVE_TOKENS)} +
+
+ Executor + + {fmt(EXEC_TOKENS)} +
+
+
+
+ {fmt(EXEC_TOKENS)}tok +
+ Source ↗ +
+ + {/* Tools exposed */} +
+
+ Tools exposed + Four integrations, log scale +
+
+
+
+ Naive + + {fmt(NAIVE_TOOLS)} +
+
+ Executor + + {fmt(EXEC_TOOLS)} +
+
+
+
+ {EXEC_TOOLS}tool +
+ Source ↗ +
+ + {/* GitHub stars: hidden when the fetch failed */} + { + stars != null && ( +
+
+ GitHub stars + UsefulSoftwareCo / executor +
+
+ 305 forks, MIT licensed +
+
{stars}
+ + Source ↗ + +
+ ) + } + + {/* npm downloads */} +
+
+ npm downloads + Package: executor +
+
+ Last 30 days, rounded +
+
70k/ 30d
+ Source ↗ +
+ + {/* Free tier */} +
+
+ Free tier executions + Unlimited integrations +
+
+ No card to start +
+
+ 100,000/ mo +
+ Source ↗ +
+ + {/* Protocols */} +
+
+ Protocols supported + One tool shape for all of them +
+
+ MCP, OpenAPI, GraphQL, Google Discovery +
+
4
+ Source ↗ +
+ + {/* Ways to run it */} +
+
+ Ways to run it + Same code paths, different packaging +
+
+ Desktop app, CLI, Executor Cloud, self-hosted Docker or Cloudflare + Worker +
+
4
+ Source ↗ +
+ + {/* Secrets */} +
+
+ Secrets in the sandbox + Injected host-side at call time +
+
+ Tool calls run in an isolated JavaScript sandbox +
+
0
+ Source ↗ +
+
+ +

Every number above links to where it came from.

+
+
diff --git a/apps/marketing/src/components/variants/proof-c.astro b/apps/marketing/src/components/variants/proof-c.astro new file mode 100644 index 0000000000..0361e66a97 --- /dev/null +++ b/apps/marketing/src/components/variants/proof-c.astro @@ -0,0 +1,121 @@ +--- +/** + * Proof section, variant C: "Before/after hero stat". + * + * One dominant figure carries the section: the prompt-size difference with + * four integrations connected, drawn as two linear bars so the ratio is the + * picture rather than a claim. Four smaller public counters sit beneath it. + */ +import "../../styles/variants/proof.css"; + +interface Props { + readonly stars: string | null; +} + +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; + +const NAIVE_TOKENS = 278_800; +const EXEC_TOKENS = 1_044; +const NAIVE_TOOLS = 1_640; +const EXEC_TOOLS = 1; + +// Linear on purpose: at this ratio the second bar is a sliver, which is the +// honest picture. A minimum width in the CSS keeps it visible. +const multiplier = Math.round(NAIVE_TOKENS / EXEC_TOKENS); +const execWidth = `${((EXEC_TOKENS / NAIVE_TOKENS) * 100).toFixed(2)}%`; + +const fmt = (n: number): string => n.toLocaleString("en-US"); +--- + +
+
+
+
In practice
+

Numbers you can check.

+

+ Connect GitHub, Stripe, Jira and Sentry. A naive setup hands the model + every tool definition up front. Executor hands it one. +

+
+ +
+
+
+
Smaller prompt
+
{multiplier}×
+
+ +
+
+
+ Without Executor + {fmt(NAIVE_TOKENS)} tok · {fmt(NAIVE_TOOLS)} tools +
+
+ +
+
+ +
+
+ With Executor + {fmt(EXEC_TOKENS)} tok · {EXEC_TOOLS} tool +
+
+ +
+
+
+
+ +
+ Figure 2. Prompt size with four integrations connected. Reproduce it in + the demo above. +
+
+ + +
+
diff --git a/apps/marketing/src/content/site-copy.ts b/apps/marketing/src/content/site-copy.ts new file mode 100644 index 0000000000..d572b15682 --- /dev/null +++ b/apps/marketing/src/content/site-copy.ts @@ -0,0 +1,183 @@ +// --------------------------------------------------------------------------- +// Shared marketing copy. One source for text that must read the same in the +// HTML homepage and in the machine-readable Markdown endpoints +// (`/index.md`, `/setup-prompt.md`, `/pricing.md`). +// +// Import from here rather than duplicating strings: the homepage renders the +// human view, the endpoints render the agent view, and both must stay in step. +// --------------------------------------------------------------------------- + +/** + * Copied to the clipboard by the "Set up with your agent" hero CTA, and served + * verbatim at `/setup-prompt.md`. A visitor pastes it into their coding agent + * (Claude, Cursor, ...) and the agent picks the right form of Executor, + * installs it, connects over MCP, and gets a first connection working. Keep in + * sync with the docs "Set up with your agent" section (apps/docs/index.mdx). + */ +export const setupPrompt = `Help me set up Executor and get my first connection working. + +Executor is an open source integration layer for AI agents: one place to configure every integration (MCP servers, OpenAPI specs, GraphQL APIs) and connect to them over MCP. + +Start by helping me pick the right form to run it in. Chat with me about it rather than jumping straight to a yes/no question, and recommend one. If I just want the fastest path, suggest Executor Cloud (free tier, nothing to install). All forms expose the same functionality, just packaged differently: + +Local (everything stays on my machine): +- Desktop app: a native app for Mac, Windows, and Linux. Best for a regular desktop environment. +- CLI (\`executor\`): best for a headless or server environment. +Both run a local HTTP server as a background service that any MCP client can connect to. + +Hosted (use it from multiple agents, including cloud ones, with nothing running locally): +- Executor Cloud: hosted, generous free tier, sign in and start immediately. +- Self-hosted: a Docker image or a Cloudflare Worker. + +How to think about it: +- Want all your data on your own machine? Go local: the desktop app for a regular environment, the CLI for a headless one. +- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker or Cloudflare versions give you full control. + +Terms you'll come across: +- Integration: anything you add (an MCP server, an OpenAPI spec, a GraphQL API). +- Connection: one configured instance of an integration. An integration can have many connections, and a connection doesn't have to be authenticated. +- Policy: whether each tool is always allowed, requires approval, or is blocked. Policies start from a sensible default derived from the imported spec (for example, GET requests on an OpenAPI spec are allowed by default). + +Once you know which form I want: +1. Walk me through installing it. +2. Connect Executor to you over MCP. Most MCP clients only load servers at startup, so after adding it I may need to restart the client or open a new chat before the Executor tools appear. Tell me if that's needed and wait for me to do it before continuing. +3. Once the tools are available, help me add my first integration and get one tool working end to end. + +Docs: https://executor.sh/docs +Source (and the place to start if something breaks): https://github.com/UsefulSoftwareCo/executor`; + +/** Canonical GitHub repository. */ +export const GITHUB_URL = "https://github.com/UsefulSoftwareCo/executor"; + +/** One-line description of the product, used as the Markdown tagline. */ +export const tagline = + "Executor is an MCP gateway. Anything that speaks MCP, like Claude Code, Cursor, or Codex, points at one endpoint and reaches every tool you connect."; + +/** Cache-Control for the Markdown endpoints. Short, so copy edits land fast. */ +export const MARKDOWN_CACHE_CONTROL = "public, max-age=300"; + +/** Content-Type for the Markdown endpoints. */ +export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8"; + +export type PricingTier = { + readonly name: string; + readonly price: string; + readonly audience: string; + readonly featuresLabel?: string; + readonly features: ReadonlyArray; + readonly cta: string; +}; + +/** + * Pricing tiers, mirroring the `#pricing` section of the homepage. `/pricing` + * redirects to that anchor, so the homepage is the human source of truth. + */ +export const pricingTiers: ReadonlyArray = [ + { + name: "Free", + price: "$0 / month", + audience: "For small teams getting started", + features: ["Up to 3 members", "100,000 executions per month", "Unlimited integrations"], + cta: "Start free: https://executor.sh/cloud", + }, + { + name: "Team", + price: "$15 / member / month", + audience: "For growing organizations (recommended)", + features: [ + "14-day free trial, then $15 / member / month", + "Unlimited executions", + "Verified domains & join by team domain", + ], + cta: "Start free trial: https://executor.sh/cloud", + }, + { + name: "Enterprise", + price: "Custom", + audience: "For orgs with custom needs", + featuresLabel: "Everything in Team, plus", + features: [ + "Self-hosted or dedicated cloud deployment support", + "SSO / SAML & SCIM provisioning", + "Audit logs for every tool call", + "Dedicated support & onboarding", + "Security reviews, DPA & SOC 2 on request", + ], + cta: "Contact rhys@executor.sh", + }, +]; + +export type Capability = { + readonly title: string; + readonly body: string; + readonly comingSoon?: boolean; +}; + +/** The six capability cards from the homepage "What you get" section. */ +export const capabilities: ReadonlyArray = [ + { + title: "One tool shape", + body: "MCP, OpenAPI, GraphQL, or a custom integration. Under the hood they all become a tool name, an input schema, and an output schema.", + }, + { + title: "Call it any way", + body: "Today it is a code-mode MCP. It could just as well be the Executor CLI, a one-off script, a gen-UI dashboard, or a reusable workflow. Same tools, every surface.", + }, + { + title: "Trace every call", + body: "One place to see every run and tool call. Audit any decision after the fact.", + comingSoon: true, + }, + { + title: "Set up once, whole team has it", + body: "Per-user credentials and shared ones. No onboarding ritual, no toggling MCPs on and off mid-task.", + }, + { + title: "Destructive actions pull you back in", + body: "Executor keeps the semantics it imported: GET vs DELETE for OpenAPI, destructiveHint for MCP, mutations for GraphQL. Agents auto-run the safe stuff and ask before the rest.", + }, + { + title: "Sandboxed execution", + body: "Tool calls run in an isolated JavaScript sandbox. Secrets are injected host-side at call time and never enter the sandbox heap, so the agent and model never see a raw token.", + }, +]; + +export type Faq = { readonly question: string; readonly answer: string }; + +/** The homepage FAQ. */ +export const faqs: ReadonlyArray = [ + { + question: "Where does my code run, and what touches my credentials?", + answer: + "Tool calls run in an isolated JavaScript sandbox. Credentials are resolved host-side at call time and injected into the outbound request only. They never enter the sandbox heap, the code your agent wrote, the agent, or the model.", + }, + { + question: "Can the agent or the model ever see a raw token?", + answer: + "No. Secrets stay host-side by design. The sandbox calls a tool by name; Executor attaches the credential to the real request outside the sandbox, so a token is never present in anything the model can read.", + }, + { + question: "What can call Executor?", + answer: + "Any MCP client (Claude Code, Cursor, Codex, and others), the Executor CLI, or a native client you drop in. Because tools share one shape, the calling surface is interchangeable.", + }, + { + question: "How does it know what is safe to auto-run?", + answer: + "Executor preserves the semantics of whatever it imported: GET vs DELETE for OpenAPI, destructiveHint for MCP, and mutations for GraphQL. That tells the agent what it can run on its own and what should pull you back into the loop.", + }, + { + question: "Is it open source? Can I self-host?", + answer: + "Yes. Executor is MIT licensed and built on the SDK we publish to npm. Run the desktop app locally, self-host the server, or use the hosted cloud. Same code paths, different deployment.", + }, +]; + +/** Response helper shared by the Markdown endpoints. */ +export const markdownResponse = (body: string): Response => + new Response(body, { + headers: { + "Content-Type": MARKDOWN_CONTENT_TYPE, + "Cache-Control": MARKDOWN_CACHE_CONTROL, + }, + }); diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index 2184b4ec12..36e3be10c4 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -6,6 +6,14 @@ import { ContextBloatDemo } from "../components/context-bloat-demo"; // the deploy pipeline does not pick up newly added /public files. import ycBackedBy from "../assets/yc-backed-by.svg?url"; import { getStars, formatStars } from "../lib/github"; +import { setupPrompt } from "../content/site-copy"; +import MissionA from "../components/variants/mission-a.astro"; +import MissionB from "../components/variants/mission-b.astro"; +import MissionC from "../components/variants/mission-c.astro"; +import ProofA from "../components/variants/proof-a.astro"; +import ProofB from "../components/variants/proof-b.astro"; +import ProofC from "../components/variants/proof-c.astro"; +import AgentsFooter from "../components/variants/agents-footer.astro"; const GH = "https://github.com/UsefulSoftwareCo/executor"; // Cloud sign-in. Same origin in prod: the cloud Worker owns executor.sh and @@ -14,42 +22,6 @@ const GH = "https://github.com/UsefulSoftwareCo/executor"; const SIGN_IN = "/api/auth/login"; const stars = await getStars(); -// Copied to the clipboard by the "Set up with your agent" hero CTA. A visitor -// pastes it into their coding agent (Claude, Cursor, ...) and the agent picks -// the right form of Executor, installs it, connects over MCP, and gets a first -// connection working. Keep in sync with the docs "Set up with your agent" -// section (apps/docs/index.mdx). -const setupPrompt = `Help me set up Executor and get my first connection working. - -Executor is an open source integration layer for AI agents: one place to configure every integration (MCP servers, OpenAPI specs, GraphQL APIs) and connect to them over MCP. - -Start by helping me pick the right form to run it in. Chat with me about it rather than jumping straight to a yes/no question, and recommend one. If I just want the fastest path, suggest Executor Cloud (free tier, nothing to install). All forms expose the same functionality, just packaged differently: - -Local (everything stays on my machine): -- Desktop app: a native app for Mac, Windows, and Linux. Best for a regular desktop environment. -- CLI (\`executor\`): best for a headless or server environment. -Both run a local HTTP server as a background service that any MCP client can connect to. - -Hosted (use it from multiple agents, including cloud ones, with nothing running locally): -- Executor Cloud: hosted, generous free tier, sign in and start immediately. -- Self-hosted: a Docker image or a Cloudflare Worker. - -How to think about it: -- Want all your data on your own machine? Go local: the desktop app for a regular environment, the CLI for a headless one. -- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker or Cloudflare versions give you full control. - -Terms you'll come across: -- Integration: anything you add (an MCP server, an OpenAPI spec, a GraphQL API). -- Connection: one configured instance of an integration. An integration can have many connections, and a connection doesn't have to be authenticated. -- Policy: whether each tool is always allowed, requires approval, or is blocked. Policies start from a sensible default derived from the imported spec (for example, GET requests on an OpenAPI spec are allowed by default). - -Once you know which form I want: -1. Walk me through installing it. -2. Connect Executor to you over MCP. Most MCP clients only load servers at startup, so after adding it I may need to restart the client or open a new chat before the Executor tools appear. Tell me if that's needed and wait for me to do it before continuing. -3. Once the tools are available, help me add my first integration and get one tool working end to end. - -Docs: https://executor.sh/docs -Source (and the place to start if something breaks): https://github.com/UsefulSoftwareCo/executor`; --- @@ -151,6 +123,14 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo
+
+
+ +
{/* ── CTA: For humans / For agents ── */}
@@ -225,6 +205,8 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo
+
+
{/* ─── WEDGE ─── */}
@@ -240,8 +222,14 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo
+
+ + + +
+ {/* ─── NO CONTEXT BLOAT (interactive) ─── */} -
+
Context efficiency
@@ -257,9 +245,24 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo
+
+
+ +
+
+
+ + + +
+ {/* ─── CAPABILITIES ─── */}
@@ -271,6 +274,8 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo
+
+
{/* One tool shape */}
@@ -367,6 +372,86 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo

+
+ +
@@ -771,8 +856,17 @@ Source (and the place to start if something breaks): https://github.com/UsefulSo Terms +
+
+ +
+ diff --git a/apps/marketing/src/pages/index.md.ts b/apps/marketing/src/pages/index.md.ts new file mode 100644 index 0000000000..24024d13f5 --- /dev/null +++ b/apps/marketing/src/pages/index.md.ts @@ -0,0 +1,144 @@ +import type { APIRoute } from "astro"; + +import { + capabilities, + faqs, + GITHUB_URL, + markdownResponse, + pricingTiers, + tagline, +} from "../content/site-copy"; + +// --------------------------------------------------------------------------- +// `/index.md` — the homepage as Markdown, for agents. +// +// Same content as src/pages/index.astro, without the layout, scripts, or +// interactive demos: an agent that fetches this gets the whole product in one +// read, plus links to the other machine-readable surfaces. +// +// The copy comes from src/content/site-copy.ts so the HTML page and this page +// cannot drift apart. +// --------------------------------------------------------------------------- + +const machineSummaries = [ + ["Docs", "https://executor.sh/docs"], + ["Setup prompt", "https://executor.sh/setup-prompt.md"], + ["Pricing", "https://executor.sh/pricing.md"], + ["llms.txt", "https://executor.sh/llms.txt"], + ["GitHub", GITHUB_URL], + ["Cloud", "https://executor.sh/cloud"], +] as const; + +const capabilityLines = capabilities.map( + ({ title, body, comingSoon }, i) => + `${i + 1}. **${title}**${comingSoon ? " _(coming soon)_" : ""} — ${body}`, +); + +const pricingLines = pricingTiers.map(({ name, price, audience, featuresLabel, features, cta }) => + [ + `### ${name} — ${price}`, + "", + audience, + "", + ...(featuresLabel === undefined ? [] : [`${featuresLabel}:`, ""]), + ...features.map((f) => `- ${f}`), + "", + cta, + ].join("\n"), +); + +const faqLines = faqs.map(({ question, answer }) => `### ${question}\n\n${answer}`); + +const body = `# Executor + +> ${tagline} + +## Machine summaries + +${machineSummaries.map(([label, href]) => `- [${label}](${href})`).join("\n")} + +## What Executor is + +Executor is an open-source integration layer for AI agents. Configure every +integration once (MCP servers, OpenAPI specs, GraphQL APIs, custom sources) +with authentication and per-tool policies, then use that one catalog from any +MCP-compatible agent. + +Wiring tools to agents is fiddly, per-client, and easy to get wrong. Executor +makes every tool, from any protocol, look the same: one name, one input schema, +one output schema, so any agent can call any of them the same way. + +Connect everything you use and Executor still shows the model a single tool. It +searches your catalog and loads a tool's schema only when the code actually +calls it, so the prompt never balloons. + +## How it works + +- **Integration** — anything you add: an MCP server, an OpenAPI spec, a GraphQL + API, or a Google Discovery document. +- **Connection** — one configured (optionally authenticated) instance of an + integration. An integration can have many connections. +- **Policy** — whether each tool is always allowed, requires approval, or is + blocked. Policies start from a sensible default derived from the imported + spec (for example, GET requests on an OpenAPI spec are allowed by default). + +Then use it from any MCP-compatible agent: one catalog of tools, shared across +every client. + +## What you get + +${capabilityLines.join("\n\n")} + +## Ways to run it + +All forms expose the same functionality, packaged differently. + +### Cloud + +Hosted Executor. Auth, sync, policies, and your whole team online in five +minutes. Free tier to start: https://executor.sh/cloud + +### Desktop + +A native app for Mac, Windows, and Linux that runs entirely on your machine. +Your integrations, credentials, and sessions never leave the device. MIT +licensed. + +Downloads follow the latest GitHub release: + +\`${GITHUB_URL}/releases/latest/download/executor-desktop-\` + +- \`executor-desktop-mac-arm64.dmg\` — Mac (Apple silicon) +- \`executor-desktop-mac-x64.dmg\` — Mac (Intel) +- \`executor-desktop-win-x64.exe\` — Windows +- \`executor-desktop-linux-x86_64.AppImage\` — Linux + +### CLI + +Run Executor as a background service and drive it from your terminal. Best for +headless and server environments. MIT licensed. + +\`\`\`sh +npm i -g executor +\`\`\` + +### Self-hosted + +A Docker image or a Cloudflare Worker: https://executor.sh/docs/hosted/docker + +## Pricing + +Start free, pay per member. Full details: https://executor.sh/pricing.md + +${pricingLines.join("\n\n")} + +## FAQ + +${faqLines.join("\n\n")} + +## Source + +Executor is MIT licensed: ${GITHUB_URL} +`; + +export const GET: APIRoute = () => markdownResponse(body); diff --git a/apps/marketing/src/pages/pricing.md.ts b/apps/marketing/src/pages/pricing.md.ts new file mode 100644 index 0000000000..d090f28894 --- /dev/null +++ b/apps/marketing/src/pages/pricing.md.ts @@ -0,0 +1,41 @@ +import type { APIRoute } from "astro"; + +import { markdownResponse, pricingTiers } from "../content/site-copy"; + +// --------------------------------------------------------------------------- +// `/pricing.md` — Executor Cloud pricing as Markdown. +// +// `/pricing` redirects to the `#pricing` section of the homepage, so the tier +// data lives in src/content/site-copy.ts and both surfaces read it. +// --------------------------------------------------------------------------- + +const tierSections = pricingTiers.map(({ name, price, audience, featuresLabel, features, cta }) => + [ + `## ${name}`, + "", + `**${price}** — ${audience}`, + "", + ...(featuresLabel === undefined ? [] : [`${featuresLabel}:`, ""]), + ...features.map((f) => `- ${f}`), + "", + cta, + ].join("\n"), +); + +const body = `# Executor pricing + +> Start free, pay per member. Pricing covers Executor Cloud; the CLI, desktop +> app, and self-hosted server are MIT licensed and free. + +${tierSections.join("\n\n")} + +## Questions + +Email rhys@executor.sh, or read the docs at https://executor.sh/docs. + +Other machine-readable pages: [/index.md](https://executor.sh/index.md), +[/setup-prompt.md](https://executor.sh/setup-prompt.md), +[/llms.txt](https://executor.sh/llms.txt). +`; + +export const GET: APIRoute = () => markdownResponse(body); diff --git a/apps/marketing/src/pages/setup-prompt.md.ts b/apps/marketing/src/pages/setup-prompt.md.ts new file mode 100644 index 0000000000..9d304983c8 --- /dev/null +++ b/apps/marketing/src/pages/setup-prompt.md.ts @@ -0,0 +1,11 @@ +import type { APIRoute } from "astro"; + +import { markdownResponse, setupPrompt } from "../content/site-copy"; + +// --------------------------------------------------------------------------- +// `/setup-prompt.md` — the exact string the homepage "Set up with your agent" +// button copies to the clipboard. An agent can fetch it instead of asking the +// visitor to paste it. +// --------------------------------------------------------------------------- + +export const GET: APIRoute = () => markdownResponse(`${setupPrompt}\n`); diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index b23ec2755a..023e5260ec 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1673,3 +1673,23 @@ body { .blog-prose h3 + p { margin-top: 0; } + +/* ── Figure captions (paper-style numbered figures) ── */ +.figcap { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.55; + color: var(--color-ink-3); + letter-spacing: 0; + text-wrap: pretty; +} +.figcap__n { + color: var(--color-ink-2); + font-weight: 500; +} + +@media (min-width: 720px) { + .cap-grid--4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/apps/marketing/src/styles/variants/agents.css b/apps/marketing/src/styles/variants/agents.css new file mode 100644 index 0000000000..4016f149ad --- /dev/null +++ b/apps/marketing/src/styles/variants/agents.css @@ -0,0 +1,91 @@ +/* --------------------------------------------------------------------------- + "For humans / For agents" footer fragment. + + Grayscale only, existing tokens (--color-ink, --color-ink-3, --color-rule). + Mono 11px uppercase, matching the eyebrow/metadata voice of the site. + All selectors are prefixed `.agents-`. + --------------------------------------------------------------------------- */ + +.agents-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; +} + +/* ── Two-segment toggle ── */ + +.agents-toggle { + display: inline-flex; + align-items: stretch; + border: 1px solid var(--color-rule); + border-radius: 5px; + overflow: hidden; +} + +.agents-toggle__item { + font-family: var(--font-mono); + font-size: 11px; + line-height: 1; + letter-spacing: 0.14em; + text-transform: uppercase; + padding: 0.5rem 0.7rem; + color: var(--color-ink-3); + text-decoration: none; + white-space: nowrap; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.agents-toggle__item + .agents-toggle__item { + border-left: 1px solid var(--color-rule); +} + +.agents-toggle__item:hover { + color: var(--color-ink); +} + +.agents-toggle__item.is-active, +.agents-toggle__item.is-active:hover { + background: var(--color-ink); + color: var(--color-surface); +} + +/* ── Markdown path list ── */ + +.agents-markdown { + font-family: var(--font-mono); + font-size: 11px; + line-height: 1.5; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-ink-3); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4em; + margin: 0; +} + +.agents-markdown__label { + color: var(--color-ink-3); +} + +.agents-markdown a { + color: var(--color-ink-3); + text-decoration: none; + text-transform: none; + letter-spacing: 0.02em; + transition: color 0.15s ease; +} + +.agents-markdown a:hover { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 2px; +} + +.agents-markdown__sep { + color: var(--color-rule); +} diff --git a/apps/marketing/src/styles/variants/mission.css b/apps/marketing/src/styles/variants/mission.css new file mode 100644 index 0000000000..63ca79936e --- /dev/null +++ b/apps/marketing/src/styles/variants/mission.css @@ -0,0 +1,257 @@ +/* ════════════════════════════════════════════════════════════════ + MISSION SECTION VARIANTS + Shared plain CSS for src/components/variants/mission-{a,b,c}.astro. + Reads a research note: hairline rules, mono metadata, inline + references. Grayscale only, on the existing custom properties. + Every class is prefixed .mission- so it cannot collide. + ═══════════════════════════════════════════════════════════════ */ + +/* ── Prose body (A and C) ── */ +.mission-prose { + display: flex; + flex-direction: column; + gap: 1.35rem; + max-width: 68ch; +} +.mission-prose p { + margin: 0; + font-size: 17px; + line-height: 1.68; + color: var(--color-ink-2); + text-wrap: pretty; +} +.mission-prose p strong, +.mission-lede { + color: var(--color-ink); + font-weight: 500; +} + +/* ── Inline reference marker: a small mono superscript numeral ── */ +.mission-ref { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + line-height: 1; + vertical-align: super; + margin-left: 1px; + padding: 0 1px; + color: var(--color-ink-3); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: + color 0.15s ease, + border-color 0.15s ease; +} +.mission-ref:hover { + color: var(--color-ink); + border-bottom-color: var(--color-rule-strong); +} + +/* ── Reference list (A): stacked, mono, 12px ── */ +.mission-refs { + margin-top: 3rem; + padding-top: 1.1rem; + border-top: 1px solid var(--color-rule); +} +.mission-refs__label { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--color-ink-3); + margin-bottom: 0.85rem; +} +.mission-refs__list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.5rem; +} +.mission-refs__list li { + display: flex; + gap: 0.6rem; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.5; + color: var(--color-ink-3); +} +.mission-refs__n { + flex-shrink: 0; + width: 1.4rem; + font-variant-numeric: tabular-nums; +} +.mission-refs__list a { + color: var(--color-ink-2); + text-decoration: none; + border-bottom: 1px solid var(--color-rule); + transition: + color 0.15s ease, + border-color 0.15s ease; +} +.mission-refs__list a:hover { + color: var(--color-ink); + border-bottom-color: var(--color-ink-3); +} +.mission-refs__src { + color: var(--color-ink-3); +} + +/* ── Compact reference row (B and C) ── */ +.mission-refs-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.55rem 1.1rem; + margin-top: 2.5rem; + padding-top: 1.1rem; + border-top: 1px solid var(--color-rule); + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-ink-3); +} +.mission-refs-row__label { + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 11px; +} +.mission-refs-row a { + color: var(--color-ink-2); + text-decoration: none; + border-bottom: 1px solid var(--color-rule); + transition: + color 0.15s ease, + border-color 0.15s ease; +} +.mission-refs-row a:hover { + color: var(--color-ink); + border-bottom-color: var(--color-ink-3); +} +.mission-refs-row__n { + color: var(--color-ink-3); + margin-right: 0.35rem; + font-variant-numeric: tabular-nums; +} + +/* ── Numbered claims (B): hairline rows, not cards ── */ +.mission-claims { + margin-top: 2.5rem; + border-top: 1px solid var(--color-rule); +} +.mission-claim { + display: grid; + grid-template-columns: 1fr; + gap: 0.5rem; + padding: 1.6rem 0; + border-bottom: 1px solid var(--color-rule); +} +@media (min-width: 720px) { + .mission-claim { + grid-template-columns: 4.5rem minmax(0, 1fr); + gap: 0 1.75rem; + align-items: baseline; + } +} +.mission-claim__num { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + color: var(--color-ink-3); + font-variant-numeric: tabular-nums; +} +.mission-claim__body p { + margin: 0; +} +.mission-claim__lead { + font-size: 17px; + font-weight: 600; + letter-spacing: -0.012em; + line-height: 1.4; + color: var(--color-ink); + margin-bottom: 0.4rem; +} +.mission-claim__support { + font-size: 15.5px; + line-height: 1.62; + color: var(--color-ink-2); + text-wrap: pretty; + max-width: 62ch; +} + +/* ── Two column with margin notes (C) ── */ +.mission-grid { + display: grid; + grid-template-columns: 1fr; + gap: 2.5rem; +} +@media (min-width: 900px) { + .mission-grid { + grid-template-columns: minmax(0, 1fr) 15.5rem; + gap: 3.5rem; + align-items: start; + } +} +.mission-notes { + display: flex; + flex-direction: column; + gap: 1.75rem; +} +@media (min-width: 900px) { + .mission-notes { + padding-left: 1.5rem; + border-left: 1px solid var(--color-rule); + } +} +.mission-note { + display: flex; + flex-direction: column; + gap: 0.55rem; +} +.mission-note__n { + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--color-ink-3); + font-variant-numeric: tabular-nums; +} +.mission-note__quote { + margin: 0; + font-size: 14.5px; + line-height: 1.55; + color: var(--color-ink); + text-wrap: pretty; +} +.mission-note__attr { + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.5; + color: var(--color-ink-3); +} +.mission-note__attr a { + color: var(--color-ink-2); + text-decoration: none; + border-bottom: 1px solid var(--color-rule); + transition: + color 0.15s ease, + border-color 0.15s ease; +} +.mission-note__attr a:hover { + color: var(--color-ink); + border-bottom-color: var(--color-ink-3); +} +.mission-note__tag { + display: inline-block; + margin-left: 0.4rem; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ink-3); + border: 1px solid var(--color-rule); + border-radius: 4px; + padding: 0 0.3rem; +} diff --git a/apps/marketing/src/styles/variants/proof.css b/apps/marketing/src/styles/variants/proof.css new file mode 100644 index 0000000000..8e1e9d631e --- /dev/null +++ b/apps/marketing/src/styles/variants/proof.css @@ -0,0 +1,389 @@ +/* ════════════════════════════════════════════════════════════════ + PROOF SECTION (variants A / B / C) + An evidence section: real, checkable numbers in the registry-grade + grayscale language. Mono uppercase captions, tabular numerals, + hairline rules, tiny inline SVG visuals. No color, no invented + metrics: every value links to where it came from. + ═══════════════════════════════════════════════════════════════ */ + +.proof-head { + margin-bottom: 3rem; +} +.proof-lede { + font-size: 16.5px; + line-height: 1.6; + color: var(--color-ink-2); + text-wrap: pretty; +} + +/* Shared metadata voice: uppercase mono captions and source links. */ +.proof-cap { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--color-ink-2); +} +.proof-sub { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; + color: var(--color-ink-3); +} +.proof-src { + display: inline-flex; + align-items: center; + gap: 0.3rem; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; + color: var(--color-ink-3); + transition: color 0.15s ease; +} +.proof-src:hover { + color: var(--color-ink); +} + +/* Numerals carry the section. Geist, tight, tabular so columns line up. */ +.proof-value { + font-family: var(--font-sans); + font-variant-numeric: tabular-nums; + font-weight: 600; + letter-spacing: -0.03em; + line-height: 1; + color: var(--color-ink); + font-size: clamp(40px, 5vw, 56px); +} +.proof-value .proof-unit { + font-size: 0.4em; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--color-ink-3); + margin-left: 0.3em; +} + +/* ── A: tile grid ───────────────────────────────────────────── */ +.proof-grid { + display: grid; + gap: 1px; + background: var(--color-rule); + border: 1px solid var(--color-rule); + border-radius: 16px; + overflow: hidden; +} +@media (min-width: 720px) { + .proof-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (min-width: 1040px) { + .proof-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} +.proof-tile { + display: flex; + flex-direction: column; + gap: 0.35rem; + padding: 1.6rem 1.5rem 1.5rem; + background: white; + min-height: 15rem; + transition: background 0.15s ease; +} +.proof-tile:hover { + background: var(--color-surface-2); +} +.proof-tile__value { + margin-top: 0.85rem; +} +.proof-tile__viz { + margin-top: auto; + padding-top: 1.1rem; +} +.proof-tile__foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: 1rem; + padding-top: 0.85rem; + border-top: 1px solid var(--color-rule); +} + +/* Paired before/after bars used inside a tile. */ +.proof-bars { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.proof-bar { + display: flex; + align-items: center; + gap: 0.6rem; + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--color-ink-3); +} +.proof-bar__lbl { + width: 3.5rem; + flex-shrink: 0; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.proof-bar__track { + position: relative; + flex: 1; + height: 8px; + background: var(--color-surface-2); + border: 1px solid var(--color-rule); + border-radius: 2px; + overflow: hidden; +} +.proof-bar__fill { + position: absolute; + inset: 0 auto 0 0; + min-width: 3px; + background: var(--color-ink-3); +} +.proof-bar--now .proof-bar__fill { + background: var(--color-ink); +} + +/* Unit squares: each square stands for a stated quantity. */ +.proof-units { + display: flex; + flex-wrap: wrap; + gap: 4px; +} +.proof-unit-sq { + width: 11px; + height: 11px; + border-radius: 2px; + background: var(--color-ink); +} +.proof-unit-sq--off { + background: transparent; + box-shadow: inset 0 0 0 1px var(--color-rule-strong); +} +.proof-glyph { + color: var(--color-ink); + opacity: 0.85; +} + +/* ── B: ledger ──────────────────────────────────────────────── */ +.proof-ledger { + border-top: 1px solid var(--color-rule); +} +.proof-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: baseline; + gap: 0.35rem 1.5rem; + padding: 1.35rem 0; + border-bottom: 1px solid var(--color-rule); +} +@media (min-width: 760px) { + .proof-row { + grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) 8rem 6.5rem; + align-items: center; + gap: 2rem; + } +} +.proof-row__head { + display: flex; + flex-direction: column; + gap: 0.25rem; +} +.proof-row__mid { + order: 3; + grid-column: 1 / -1; +} +@media (min-width: 760px) { + .proof-row__mid { + order: 0; + grid-column: auto; + } +} +.proof-row__val { + text-align: right; + font-family: var(--font-sans); + font-variant-numeric: tabular-nums; + font-size: 22px; + font-weight: 600; + letter-spacing: -0.02em; + color: var(--color-ink); + white-space: nowrap; +} +.proof-row__val .proof-unit { + font-size: 11px; + font-family: var(--font-mono); + font-weight: 400; + letter-spacing: 0.04em; + color: var(--color-ink-3); + margin-left: 0.45rem; +} +.proof-row__src { + order: 4; + grid-column: 1 / -1; + justify-self: start; +} +@media (min-width: 760px) { + .proof-row__src { + order: 0; + grid-column: auto; + justify-self: end; + } +} +.proof-foot { + margin-top: 1.6rem; + font-family: var(--font-mono); + font-size: 12px; + letter-spacing: 0.02em; + color: var(--color-ink-3); +} + +/* ── C: before/after hero stat ──────────────────────────────── */ +.proof-hero { + border: 1px solid var(--color-rule); + border-radius: 16px; + background: white; + overflow: hidden; +} +.proof-hero__top { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1.75rem 1.75rem 1.6rem; + border-bottom: 1px solid var(--color-rule); +} +@media (min-width: 820px) { + .proof-hero__top { + flex-direction: row; + align-items: center; + gap: 3rem; + padding: 2.25rem 2.25rem 2rem; + } +} +.proof-hero__mult { + flex-shrink: 0; +} +.proof-hero__mult .proof-value { + font-size: clamp(56px, 9vw, 92px); + margin-top: 0.5rem; +} +.proof-hero__cmp { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1.1rem; +} +.proof-cmp-row { + display: flex; + flex-direction: column; + gap: 0.45rem; +} +.proof-cmp-row__line { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + font-family: var(--font-mono); + font-size: 11.5px; + letter-spacing: 0.02em; + color: var(--color-ink-2); +} +.proof-cmp-row__line b { + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-ink-2); +} +.proof-cmp-row__line span { + font-variant-numeric: tabular-nums; + color: var(--color-ink-3); + white-space: nowrap; +} +.proof-cmp-track { + position: relative; + height: 14px; + background: var(--color-surface-2); + border: 1px solid var(--color-rule); + border-radius: 3px; + overflow: hidden; +} +.proof-cmp-fill { + position: absolute; + inset: 0 auto 0 0; + min-width: 4px; + background: var(--color-ink-3); +} +.proof-cmp-row--now .proof-cmp-fill { + background: var(--color-ink); +} +.proof-figcap { + padding: 0.95rem 1.75rem 1.1rem; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.5; + letter-spacing: 0.01em; + color: var(--color-ink-3); +} +@media (min-width: 820px) { + .proof-figcap { + padding: 0.95rem 2.25rem 1.1rem; + } +} +.proof-figcap a { + color: var(--color-ink-2); + text-decoration: underline; + text-underline-offset: 3px; +} +.proof-figcap a:hover { + color: var(--color-ink); +} + +.proof-mini-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + margin-top: 1.5rem; + background: var(--color-rule); + border: 1px solid var(--color-rule); + border-radius: 14px; + overflow: hidden; +} +@media (min-width: 820px) { + .proof-mini-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +.proof-mini { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1.25rem 1.25rem 1.15rem; + background: white; + transition: background 0.15s ease; +} +.proof-mini:hover { + background: var(--color-surface-2); +} +.proof-mini__val { + font-family: var(--font-sans); + font-variant-numeric: tabular-nums; + font-size: 30px; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1; + color: var(--color-ink); +} +.proof-mini__val .proof-unit { + font-size: 12px; + font-family: var(--font-mono); + font-weight: 400; + letter-spacing: 0.04em; + color: var(--color-ink-3); + margin-left: 0.4rem; +} From 537fcbca438a25c60ecf295089cfe4e4754f6b2f Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:59:38 -0700 Subject: [PATCH 02/37] marketing: sidebar rail layout variants Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/mission-a.astro | 2 +- .../src/components/variants/mission-b.astro | 2 +- .../src/components/variants/mission-c.astro | 2 +- .../src/components/variants/rail-a.astro | 97 +++++ .../src/components/variants/rail-b.astro | 89 +++++ .../src/components/variants/rail-c.astro | 60 +++ .../src/components/variants/rail-spy.ts | 80 ++++ apps/marketing/src/pages/index.astro | 28 +- apps/marketing/src/styles/global.css | 61 +++ apps/marketing/src/styles/variants/rail.css | 371 ++++++++++++++++++ 10 files changed, 784 insertions(+), 8 deletions(-) create mode 100644 apps/marketing/src/components/variants/rail-a.astro create mode 100644 apps/marketing/src/components/variants/rail-b.astro create mode 100644 apps/marketing/src/components/variants/rail-c.astro create mode 100644 apps/marketing/src/components/variants/rail-spy.ts create mode 100644 apps/marketing/src/styles/variants/rail.css diff --git a/apps/marketing/src/components/variants/mission-a.astro b/apps/marketing/src/components/variants/mission-a.astro index a31c95751a..1b921dd8d8 100644 --- a/apps/marketing/src/components/variants/mission-a.astro +++ b/apps/marketing/src/components/variants/mission-a.astro @@ -6,7 +6,7 @@ import "../../styles/variants/mission.css"; --- -
+
Mission
diff --git a/apps/marketing/src/components/variants/mission-b.astro b/apps/marketing/src/components/variants/mission-b.astro index 1c5397940c..1bd56ae1df 100644 --- a/apps/marketing/src/components/variants/mission-b.astro +++ b/apps/marketing/src/components/variants/mission-b.astro @@ -5,7 +5,7 @@ import "../../styles/variants/mission.css"; --- -
+
Mission

diff --git a/apps/marketing/src/components/variants/mission-c.astro b/apps/marketing/src/components/variants/mission-c.astro index a0de36c737..b6d0657068 100644 --- a/apps/marketing/src/components/variants/mission-c.astro +++ b/apps/marketing/src/components/variants/mission-c.astro @@ -6,7 +6,7 @@ import "../../styles/variants/mission.css"; --- -
+
Mission
diff --git a/apps/marketing/src/components/variants/rail-a.astro b/apps/marketing/src/components/variants/rail-a.astro new file mode 100644 index 0000000000..39a485e5cb --- /dev/null +++ b/apps/marketing/src/components/variants/rail-a.astro @@ -0,0 +1,97 @@ +--- +// Rail, variant A: "Paper rail". +// Full left sidebar: wordmark, primary nav with an active tab bar, the star +// pill and the primary CTA, then a ticked "On this page" index. The page shell +// owns the column and its sticky behaviour. +import "../../styles/variants/rail.css"; + +interface Props { + stars: string | null; +} +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const SIGN_IN = "/api/auth/login"; + +const sections = [ + { id: "mission", label: "Mission" }, + { id: "context", label: "Context" }, + { id: "proof", label: "In practice" }, + { id: "capabilities", label: "What you get" }, + { id: "install", label: "Get started" }, + { id: "faq", label: "FAQ" }, + { id: "pricing", label: "Pricing" }, +]; +--- + + + + diff --git a/apps/marketing/src/components/variants/rail-b.astro b/apps/marketing/src/components/variants/rail-b.astro new file mode 100644 index 0000000000..4bbf4578ff --- /dev/null +++ b/apps/marketing/src/components/variants/rail-b.astro @@ -0,0 +1,89 @@ +--- +// Rail, variant B: "Gridline rail". +// Compact and technical: mark-only wordmark, a tight link stack, an outlined +// CTA, and a numbered mono index. A full-height hairline closes the right edge. +import "../../styles/variants/rail.css"; + +interface Props { + stars: string | null; +} +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const SIGN_IN = "/api/auth/login"; + +const sections = [ + { id: "mission", label: "Mission" }, + { id: "context", label: "Context" }, + { id: "proof", label: "In practice" }, + { id: "capabilities", label: "What you get" }, + { id: "install", label: "Get started" }, + { id: "faq", label: "FAQ" }, + { id: "pricing", label: "Pricing" }, +]; +const pad = (index: number) => String(index + 1).padStart(2, "0"); +--- + + + + diff --git a/apps/marketing/src/components/variants/rail-c.astro b/apps/marketing/src/components/variants/rail-c.astro new file mode 100644 index 0000000000..8e3ff8ac09 --- /dev/null +++ b/apps/marketing/src/components/variants/rail-c.astro @@ -0,0 +1,60 @@ +--- +// Rail, variant C: "Minimal rail". +// Five section links carry the whole rail, so they double as the index. The +// secondary links sit below a hairline. No CTA here: the hero already has one. +import "../../styles/variants/rail.css"; + +interface Props { + stars: string | null; +} +const { stars } = Astro.props; + +const GH = "https://github.com/UsefulSoftwareCo/executor"; +const SIGN_IN = "/api/auth/login"; + +const sections = [ + { id: "mission", label: "What it is" }, + { id: "context", label: "Context" }, + { id: "install", label: "Get started" }, + { id: "pricing", label: "Pricing" }, + { id: "faq", label: "FAQ" }, +]; +--- + + + + diff --git a/apps/marketing/src/components/variants/rail-spy.ts b/apps/marketing/src/components/variants/rail-spy.ts new file mode 100644 index 0000000000..49567f823b --- /dev/null +++ b/apps/marketing/src/components/variants/rail-spy.ts @@ -0,0 +1,80 @@ +// Scroll-spy for the sidebar rail variants. +// +// Each rail renders a `.rail__toc` whose links point at the page's section +// anchors. This marks the link for the section currently in the reading band +// with `aria-current="true"`, and mirrors the id onto the aside as +// `data-active-section` so CSS can react without extra classes. +// +// Rails can be mounted inside a hidden picker wrapper, and the picker can swap +// which one is visible at any time. So: bind unconditionally (an observer on a +// hidden subtree is cheap and harmless), tolerate missing sections, and stay +// idempotent if the initializer runs again on the same element. + +const BOUND = "railSpyBound"; + +export function initRailSpy(root: HTMLElement): void { + if (root.dataset[BOUND] === "1") return; + root.dataset[BOUND] = "1"; + + const links = Array.from(root.querySelectorAll(".rail__toc a[href^='#']")); + if (links.length === 0) return; + + // Pair each link with its section. A link whose target is absent (for + // example #proof when no proof variant is picked) is hidden, not left + // pointing at nothing. + const pairs: Array<{ id: string; link: HTMLAnchorElement }> = []; + for (const link of links) { + const id = decodeURIComponent(link.getAttribute("href")!.slice(1)); + // Picker wrappers can hold several copies of a section under one id; only + // the visible copy ever intersects, so observe all of them. + const sections = id ? document.querySelectorAll(`[id="${CSS.escape(id)}"]`) : []; + if (sections.length === 0) { + const row = link.closest(".rail__toc-item") ?? link; + row.style.display = "none"; + continue; + } + pairs.push({ id, link }); + } + if (pairs.length === 0) return; + + const order = pairs.map((p) => p.id); + const visible = new Set(); + + const apply = (): void => { + // Topmost visible section wins, so the rail never flickers between two + // sections that overlap the reading band. + let activeId: string | null = null; + for (const id of order) { + if (visible.has(id)) { + activeId = id; + break; + } + } + if (activeId == null) { + delete root.dataset.activeSection; + } else { + root.dataset.activeSection = activeId; + } + for (const { id, link } of pairs) { + if (id === activeId) link.setAttribute("aria-current", "true"); + else link.removeAttribute("aria-current"); + } + }; + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) visible.add(entry.target.id); + else visible.delete(entry.target.id); + } + apply(); + }, + { rootMargin: "-40% 0px -55% 0px", threshold: 0 }, + ); + + for (const { id } of pairs) { + for (const section of document.querySelectorAll(`[id="${CSS.escape(id)}"]`)) { + observer.observe(section); + } + } +} diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index 36e3be10c4..d9982a53a3 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -14,6 +14,9 @@ import ProofA from "../components/variants/proof-a.astro"; import ProofB from "../components/variants/proof-b.astro"; import ProofC from "../components/variants/proof-c.astro"; import AgentsFooter from "../components/variants/agents-footer.astro"; +import RailA from "../components/variants/rail-a.astro"; +import RailB from "../components/variants/rail-b.astro"; +import RailC from "../components/variants/rail-c.astro"; const GH = "https://github.com/UsefulSoftwareCo/executor"; // Cloud sign-in. Same origin in prod: the cloud Worker owns executor.sh and @@ -38,7 +41,9 @@ const stars = await getStars(); >
-
+
+
+
{/* ─── NAV ─── */} +
+ + + +
+
{/* ─── HERO ─── */}
-
+

@@ -208,7 +225,7 @@ const stars = await getStars();
{/* ─── WEDGE ─── */} -
+

{/* ─── CAPABILITIES ─── */} -

+
What you get
@@ -580,7 +597,7 @@ const stars = await getStars();
{/* ─── FAQ ─── */} -
+

FAQ

@@ -865,6 +882,7 @@ const stars = await getStars();
+
diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index 023e5260ec..139b6b9703 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1693,3 +1693,64 @@ body { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +/* ── Sidebar ("rail") layout. Active when a rail option is visible. ── */ +.page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) { + --rail-w: 280px; +} +.page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail--b) { + --rail-w: 240px; +} +.page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail--c) { + --rail-w: 220px; +} +@media (min-width: 1024px) { + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) { + display: grid; + grid-template-columns: var(--rail-w) minmax(0, 1fr); + align-items: start; + } + .page__rail { + position: sticky; + top: 0; + height: 100dvh; + overflow: hidden auto; + } + /* Content sits against the rail with a fixed gutter, like a document. */ + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body { + max-width: 1120px; + padding-left: clamp(24px, 4vw, 72px); + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner { + text-align: left; + margin-left: 0; + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .mx-auto { + margin-left: 0; + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .hero-cta, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .audience-pane { + justify-content: flex-start; + align-items: flex-start; + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner h1 { + font-size: clamp(2.6rem, 5vw, 4.4rem); + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center { + text-align: left; + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center .mx-auto, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center.mx-auto { + margin-left: 0; + } + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .pt-24, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body > section:first-child > .relative { + padding-top: 4.5rem; + } +} +@media (min-width: 1024px) { + /* In rail mode every section shares the hero's left edge, like a document. */ + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body [class*="max-w-"].mx-auto { + margin-left: 0; + } +} diff --git a/apps/marketing/src/styles/variants/rail.css b/apps/marketing/src/styles/variants/rail.css new file mode 100644 index 0000000000..de8d70df6a --- /dev/null +++ b/apps/marketing/src/styles/variants/rail.css @@ -0,0 +1,371 @@ +/* ═══════════════════════════════════════════════════════════════ + Sidebar rail variants + The page shell owns the column (sticky, 100dvh, its own width). + Everything here is internal to the rail. + A "Paper rail" 280px full nav + ticked "On this page" + B "Gridline rail" 240px compact, numbered mono toc, right rule + C "Minimal rail" 220px five section links, no toc label + ═══════════════════════════════════════════════════════════════ */ + +.rail { + display: flex; + flex-direction: column; + height: 100%; + background: var(--color-surface); + font-family: var(--font-sans); +} + +/* Everything below the wordmark. On desktop it is the vertical body of the + rail; on mobile it becomes the right-hand cluster of the top bar. */ +.rail__stack { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; +} + +/* ── Wordmark ── */ +.rail__brand { + display: flex; + align-items: center; + gap: 10px; +} +.rail__brand-mark { + width: 20px; + height: 20px; + flex-shrink: 0; +} +.rail__brand-name { + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--color-ink); +} + +/* ── Primary / secondary link lists ── */ +.rail__nav { + display: flex; + flex-direction: column; +} +.rail__link { + position: relative; + display: flex; + align-items: center; + gap: 6px; + color: var(--color-ink-2); + transition: color 0.15s ease; +} +.rail__link:hover { + color: var(--color-ink); +} +.rail__link[aria-current="page"] { + color: var(--color-ink); + font-weight: 500; +} +.rail__ext { + font-size: 0.85em; + color: var(--color-ink-3); +} + +/* ── Shared toc scaffolding ── */ +.rail__toc { + display: flex; + flex-direction: column; +} +.rail__toc-item { + display: block; +} +.rail__toc a { + display: flex; + align-items: center; + color: var(--color-ink-2); + transition: color 0.15s ease; +} +.rail__toc a:hover { + color: var(--color-ink); +} + +/* ── Foot ── */ +.rail__foot { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-ink-3); + transition: color 0.15s ease; +} +.rail__foot:hover { + color: var(--color-ink-2); +} + +/* ═══ A: Paper rail ═══════════════════════════════════════════ */ +.rail--a, +.rail--a .rail__stack { + gap: 32px; +} +.rail--a { + padding: 40px 32px; +} +.rail--a .rail__nav { + gap: 2px; +} +.rail--a .rail__link { + font-size: 14px; + padding: 6px 0 6px 14px; +} +/* Active tab bar, drawn as a pseudo-element so the text never shifts. */ +.rail--a .rail__link::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 2px; + height: 14px; + background: var(--color-ink); + opacity: 0; + transition: opacity 0.15s ease; +} +.rail--a .rail__link[aria-current="page"]::before { + opacity: 1; +} +.rail--a .rail__cta { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 10px; + margin-top: 18px; +} +.rail--a .gh-star { + justify-content: flex-start; +} +.rail--a .rail__cta .btn-primary { + text-align: center; +} +.rail--a .rail__divider { + border-top: 1px dashed var(--color-rule-strong); +} +.rail--a .rail__toc-label { + margin-bottom: 16px; +} +.rail--a .rail__toc { + gap: 4px; +} +.rail--a .rail__toc a { + gap: 10px; + padding: 5px 0; + font-size: 13.5px; +} +.rail--a .rail__tick { + flex-shrink: 0; + width: 12px; + height: 1px; + background: var(--color-ink-3); + transition: + width 0.2s ease, + height 0.2s ease, + background 0.2s ease; +} +.rail--a .rail__toc a[aria-current="true"] { + color: var(--color-ink); + font-weight: 500; +} +.rail--a .rail__toc a[aria-current="true"] .rail__tick { + width: 20px; + height: 2px; + background: var(--color-ink); +} +.rail--a .rail__foot { + align-self: flex-start; + margin-top: auto; +} + +/* ═══ B: Gridline rail ════════════════════════════════════════ */ +.rail--b, +.rail--b .rail__stack { + gap: 28px; +} +.rail--b { + padding: 32px 24px; + border-right: 1px solid var(--color-rule); +} +.rail--b .rail__brand-mark { + width: 18px; + height: 18px; +} +.rail--b .rail__nav { + gap: 4px; +} +.rail--b .rail__link { + font-size: 14px; + padding-left: 12px; +} +.rail--b .rail__link::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 14px; + background: var(--color-ink); + opacity: 0; + transition: opacity 0.15s ease; +} +.rail--b .rail__link[aria-current="page"]::before { + opacity: 1; +} +.rail--b .rail__cta { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + margin-top: 16px; +} +.rail--b .rail__box { + display: inline-flex; + align-items: center; + border: 1px solid var(--color-ink); + border-radius: 4px; + padding: 4px 10px; + font-size: 13px; + color: var(--color-ink); + transition: + background 0.15s ease, + color 0.15s ease; +} +.rail--b .rail__box:hover { + background: var(--color-ink); + color: var(--color-surface); +} +.rail--b .rail__toc { + gap: 8px; +} +.rail--b .rail__toc a { + gap: 8px; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.04em; + color: var(--color-ink-3); +} +.rail--b .rail__num { + font-variant-numeric: tabular-nums; + color: var(--color-ink-3); + transition: color 0.15s ease; +} +.rail--b .rail__toc a[aria-current="true"], +.rail--b .rail__toc a[aria-current="true"] .rail__num { + color: var(--color-ink); +} +.rail--b .rail__foot { + align-self: flex-start; + margin-top: auto; +} + +/* ═══ C: Minimal rail ═════════════════════════════════════════ */ +.rail--c, +.rail--c .rail__stack { + gap: 36px; +} +.rail--c { + padding: 48px 32px; +} +.rail--c .rail__toc { + gap: 12px; +} +.rail--c .rail__toc a { + position: relative; + padding-left: 14px; + font-size: 16px; + letter-spacing: -0.01em; +} +.rail--c .rail__toc a::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 1.5px; + height: 15px; + background: var(--color-ink); + opacity: 0; + transition: opacity 0.15s ease; +} +.rail--c .rail__toc a[aria-current="true"] { + color: var(--color-ink); +} +.rail--c .rail__toc a[aria-current="true"]::before { + opacity: 1; +} +.rail--c .rail__divider { + border-top: 1px solid var(--color-rule); +} +.rail--c .rail__nav { + gap: 10px; +} +.rail--c .rail__link { + font-size: 13.5px; +} +.rail--c .rail__count { + font-family: var(--font-mono); + font-size: 12px; + font-variant-numeric: tabular-nums; + color: var(--color-ink-3); +} + +/* ═══ Mobile: every rail collapses to one horizontal top bar ═══ */ +@media (max-width: 1023.98px) { + .rail { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 16px; + height: 56px; + padding: 0 24px; + border-right: 0; + border-bottom: 1px solid var(--color-rule); + } + .rail .rail__toc, + .rail .rail__toc-label, + .rail .rail__divider, + .rail .rail__foot, + .rail .rail__secondary { + display: none; + } + .rail .rail__nav { + flex-direction: row; + align-items: center; + gap: 20px; + } + .rail .rail__cta { + flex-direction: row; + align-items: center; + gap: 12px; + margin-top: 0; + } + .rail .rail__stack { + flex: 0 1 auto; + flex-direction: row; + align-items: center; + justify-content: flex-end; + gap: 20px; + } + .rail .rail__link { + padding: 0 0 0 0; + font-size: 14px; + } + .rail .rail__link::before { + display: none; + } + /* The star pill and the mono count are the first things to go when the bar + gets tight; the links themselves survive longest. */ + .rail .gh-star { + display: none; + } +} + +@media (max-width: 719.98px) { + .rail .rail__nav .rail__link:not(.rail__link--keep) { + display: none; + } +} From a8337a36bda9e02e94df8d480d19f7aec12ec8a2 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:02:56 -0700 Subject: [PATCH 03/37] marketing: default to paper rail, fallback variant switcher Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/pages/index.astro | 33 ++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index d9982a53a3..fcf770d164 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -43,7 +43,7 @@ const stars = await getStars();
-
+ - + From 1a27bb722f0443d7d91f60078d705ecbf597ce4f Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:06:47 -0700 Subject: [PATCH 04/37] marketing: document-style homepage body Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 188 ++++++++++++++++++ .../src/components/variants/rail-a.astro | 7 +- .../src/components/variants/rail-b.astro | 7 +- .../src/components/variants/rail-c.astro | 11 +- .../src/components/variants/rail-spy.ts | 17 ++ apps/marketing/src/pages/index.astro | 9 + apps/marketing/src/styles/variants/doc.css | 188 ++++++++++++++++++ 7 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 apps/marketing/src/components/variants/doc-body.astro create mode 100644 apps/marketing/src/styles/variants/doc.css diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro new file mode 100644 index 0000000000..39c767dfc5 --- /dev/null +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -0,0 +1,188 @@ +--- +// Document-style homepage body. Reads like a short paper about the product: +// one modest headline, plain first-person prose, six beats separated by +// hairlines, the two demos kept as numbered figures, and one way to start. +// Sits inside the rail layout; the rail's "On this page" list points at the +// section ids below. +import { AnimatedBeamDemo } from "../animated-beam-demo"; +import { ContextBloatDemo } from "../context-bloat-demo"; +import { setupPrompt } from "../../content/site-copy"; +import "../../styles/variants/doc.css"; + +interface Props { + gh: string; +} +const { gh } = Astro.props; +--- + +
+ {/* ── Intro ── */} +
+

+ Executor is an MCP gateway. Your agent points at one endpoint and reaches every tool you connect. +

+

+ Claude Code, Cursor, Codex, or anything else that speaks MCP. Any tool from any + protocol. One place to add it, authenticate it, and decide what it is allowed to do. +

+
+ Start on Cloud + + Source on GitHub ↗ +
+

+ Works with Claude Code, Cursor, Codex, ChatGPT, and any MCP client. +

+
+ + {/* ── 01 Mission ── */} +
+

What we think

+

+ Every agent client re-implements every integration. The same API gets wired up again + for each new tool, with its own auth and its own failure modes. Credentials end up + pasted in five places, and nothing shares an idea of what a tool is allowed to do. +

+

+ We think tools should belong to you, not to one client or one model provider. So + Executor gives every tool one shape: a name, an input schema, an output schema. An + MCP server, an OpenAPI spec, and a GraphQL API all end up the same, and any agent + calls any of them the same way. +

+

+ We also think the safe path has to be the easy path. Executor keeps the semantics it + imported, so a GET and a DELETE are not the same thing, and it runs every call in a + sandbox where a raw token never reaches the model. +

+
+
+ +
+
+ Figure 1. Three clients reach six tools through one + MCP endpoint. Each tool keeps the protocol it came from. +
+
+
+ + {/* ── 02 Context ── */} +
+

Thousands of tools, one in the prompt

+

+ Executor shows the model a single tool. It searches your catalog and loads a tool's + schema only when the code calls it, so connecting more services does not grow the + prompt. Toggle the services below to see the difference. +

+
+ +
+ Figure 2. Prompt size with your selection of + integrations connected, with and without Executor. +
+
+
+ + {/* ── 03 Safety ── */} +
+

Safe by default

+
+
+
Policies come from the source.
+
+ GET versus DELETE for OpenAPI, destructiveHint for MCP, mutations for GraphQL. + Agents run the safe calls on their own and ask before the rest. You can override + any tool. +
+
+
+
Secrets never reach the model.
+
+ Calls run in an isolated JavaScript sandbox. Credentials are attached host-side + at call time and never enter the sandbox, the agent, or the model's context. +
+
+
+
Set up once, whole team has it.
+
+ Per-user credentials and shared ones. New teammates get the catalog on day one, + with the same policies. +
+
+
+
+ + {/* ── 04 Run it ── */} +
+

Run it where you want

+

+ Executor is MIT licensed. All forms expose the same tools, packaged differently. +

+
    +
  1. + 01 +
    + Cloud. Hosted, free to start, nothing to install. + executor.sh/cloud ↗ +
    +
  2. +
  3. + 02 +
    + Desktop. A native app for Mac, Windows, and Linux. Everything + stays on your machine. + Latest release ↗ +
    +
  4. +
  5. + 03 +
    + CLI. A background service for headless and server environments. + npm i -g executor +
    +
  6. +
  7. + 04 +
    + Self-hosted. A Docker image or a Cloudflare Worker. + Docs ↗ +
    +
  8. +
+
+ + {/* ── 05 Pricing ── */} +
+

Pricing

+

Cloud is free for up to three members and 100,000 executions a month. Team is $15 per member per month with unlimited executions. Enterprise adds self-hosted support, SSO and SCIM, and audit logs.

+

Full pricing ↗ · Talk to us

+
+ + {/* ── 06 Writing ── */} +
+

Writing

+ +
+ + {/* ── 07 About ── */} +
+

About

+

+ We are a small team backed by Y Combinator. We built Executor because we wanted our + own agents to reach our company's resources in a way that was not scary. Most setups + make you choose between locked down and useless, or wide open and risky. We wanted a + third option. +

+

+ More about Executor ↗ · Discord ↗ +

+
+
diff --git a/apps/marketing/src/components/variants/rail-a.astro b/apps/marketing/src/components/variants/rail-a.astro index 39a485e5cb..f2ff57b10e 100644 --- a/apps/marketing/src/components/variants/rail-a.astro +++ b/apps/marketing/src/components/variants/rail-a.astro @@ -14,13 +14,16 @@ const GH = "https://github.com/UsefulSoftwareCo/executor"; const SIGN_IN = "/api/auth/login"; const sections = [ - { id: "mission", label: "Mission" }, + { id: "mission", label: "What we think" }, { id: "context", label: "Context" }, { id: "proof", label: "In practice" }, { id: "capabilities", label: "What you get" }, - { id: "install", label: "Get started" }, + { id: "safety", label: "Safety" }, + { id: "install", label: "Run it" }, { id: "faq", label: "FAQ" }, { id: "pricing", label: "Pricing" }, + { id: "writing", label: "Writing" }, + { id: "about", label: "About" }, ]; --- diff --git a/apps/marketing/src/components/variants/rail-b.astro b/apps/marketing/src/components/variants/rail-b.astro index 4bbf4578ff..f3936dfb5c 100644 --- a/apps/marketing/src/components/variants/rail-b.astro +++ b/apps/marketing/src/components/variants/rail-b.astro @@ -13,13 +13,16 @@ const GH = "https://github.com/UsefulSoftwareCo/executor"; const SIGN_IN = "/api/auth/login"; const sections = [ - { id: "mission", label: "Mission" }, + { id: "mission", label: "What we think" }, { id: "context", label: "Context" }, { id: "proof", label: "In practice" }, { id: "capabilities", label: "What you get" }, - { id: "install", label: "Get started" }, + { id: "safety", label: "Safety" }, + { id: "install", label: "Run it" }, { id: "faq", label: "FAQ" }, { id: "pricing", label: "Pricing" }, + { id: "writing", label: "Writing" }, + { id: "about", label: "About" }, ]; const pad = (index: number) => String(index + 1).padStart(2, "0"); --- diff --git a/apps/marketing/src/components/variants/rail-c.astro b/apps/marketing/src/components/variants/rail-c.astro index 8e3ff8ac09..c4dbe838ca 100644 --- a/apps/marketing/src/components/variants/rail-c.astro +++ b/apps/marketing/src/components/variants/rail-c.astro @@ -13,11 +13,16 @@ const GH = "https://github.com/UsefulSoftwareCo/executor"; const SIGN_IN = "/api/auth/login"; const sections = [ - { id: "mission", label: "What it is" }, + { id: "mission", label: "What we think" }, { id: "context", label: "Context" }, - { id: "install", label: "Get started" }, - { id: "pricing", label: "Pricing" }, + { id: "proof", label: "In practice" }, + { id: "capabilities", label: "What you get" }, + { id: "safety", label: "Safety" }, + { id: "install", label: "Run it" }, { id: "faq", label: "FAQ" }, + { id: "pricing", label: "Pricing" }, + { id: "writing", label: "Writing" }, + { id: "about", label: "About" }, ]; --- diff --git a/apps/marketing/src/components/variants/rail-spy.ts b/apps/marketing/src/components/variants/rail-spy.ts index 49567f823b..1e7ff882c3 100644 --- a/apps/marketing/src/components/variants/rail-spy.ts +++ b/apps/marketing/src/components/variants/rail-spy.ts @@ -37,6 +37,23 @@ export function initRailSpy(root: HTMLElement): void { } if (pairs.length === 0) return; + // A target that only exists inside a hidden picker wrapper is not on the + // page the reader sees; hide its row, and re-check when wrappers toggle. + const refreshRows = (): void => { + for (const { id, link } of pairs) { + const targets = document.querySelectorAll(`[id="${CSS.escape(id)}"]`); + const shown = Array.from(targets).some((t) => t.closest("[hidden]") == null); + const row = link.closest(".rail__toc-item") ?? link; + row.style.display = shown ? "" : "none"; + } + }; + refreshRows(); + new MutationObserver(refreshRows).observe(document.body, { + attributes: true, + attributeFilter: ["hidden"], + subtree: true, + }); + const order = pairs.map((p) => p.id); const visible = new Set(); diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index fcf770d164..912d81d6e0 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -17,6 +17,7 @@ import AgentsFooter from "../components/variants/agents-footer.astro"; import RailA from "../components/variants/rail-a.astro"; import RailB from "../components/variants/rail-b.astro"; import RailC from "../components/variants/rail-c.astro"; +import DocBody from "../components/variants/doc-body.astro"; const GH = "https://github.com/UsefulSoftwareCo/executor"; // Cloud sign-in. Same origin in prod: the cloud Worker owns executor.sh and @@ -119,6 +120,8 @@ const stars = await getStars();
+
+

+
+
+ +
+
+ {/* ─── FOOTER ─── */}
p { + font-size: 16px; + line-height: 1.65; + color: var(--color-ink-2); + text-wrap: pretty; +} +.doc__section > p + p { + margin-top: 1rem; +} +.doc__section a { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--color-rule-strong); +} +.doc__section a:hover { + text-decoration-color: var(--color-ink); +} + +.doc__fig { + margin-top: 1.75rem; +} +.doc__fig .figcap { + margin-top: 0.85rem; +} +.doc__fig--wide { + margin-top: 1.5rem; +} +/* The context demo is designed for a wide canvas; let it break out of the + prose measure on large screens. */ +@media (min-width: 1200px) { + .doc__fig--wide { + width: min(1040px, calc(100vw - var(--rail-w, 0px) - 6rem)); + } +} + +.doc__beats { + display: grid; + gap: 1.25rem; +} +.doc__beats dt { + font-size: 16px; + font-weight: 600; + color: var(--color-ink); + margin-bottom: 0.2rem; +} +.doc__beats dd { + font-size: 15.5px; + line-height: 1.6; + color: var(--color-ink-2); +} + +.doc__ways { + margin-top: 1.25rem; + display: grid; + gap: 0; +} +.doc__ways li { + display: grid; + grid-template-columns: 2.5rem 1fr; + gap: 0.5rem; + padding: 0.85rem 0; + border-top: 1px solid var(--color-rule); + font-size: 15.5px; + line-height: 1.55; + color: var(--color-ink-2); +} +.doc__ways li:last-child { + border-bottom: 1px solid var(--color-rule); +} +.doc__ways strong { + color: var(--color-ink); + font-weight: 600; +} +.doc__ways a, +.doc__ways code { + margin-left: 0.4rem; +} +.doc__ways code { + font-family: var(--font-mono); + font-size: 12.5px; + background: var(--color-surface-2); + border: 1px solid var(--color-rule); + border-radius: 5px; + padding: 0.1rem 0.4rem; + color: var(--color-ink); +} +.doc__num { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.08em; + color: var(--color-ink-3); + padding-top: 0.25rem; +} + +.doc__list { + display: grid; + gap: 0; +} +.doc__list li { + padding: 0.7rem 0; + border-top: 1px solid var(--color-rule); + font-size: 15.5px; +} +.doc__list li:last-child { + border-bottom: 1px solid var(--color-rule); +} +.doc__quiet-p { + margin-top: 0.75rem; + font-size: 14px !important; + color: var(--color-ink-3) !important; +} From 38bef091e7d95c9b932ff7673be6d0dd1a97e5de Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:01:45 -0700 Subject: [PATCH 05/37] marketing: centre the document column beside the rail Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/styles/global.css | 4 ++-- apps/marketing/src/styles/variants/doc.css | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index 139b6b9703..5f93cedf0f 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1718,8 +1718,8 @@ body { } /* Content sits against the rail with a fixed gutter, like a document. */ .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body { - max-width: 1120px; - padding-left: clamp(24px, 4vw, 72px); + max-width: none; + padding-left: 0; } .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner { text-align: left; diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index 86140e24e2..2fc2be8e4d 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -9,8 +9,11 @@ } } @media (min-width: 1024px) { + /* Centre the column in the space beside the rail. */ .doc { - padding-left: 0; + margin-inline: auto; + padding-inline: 2.5rem; + max-width: calc(720px + 5rem); } } @@ -107,6 +110,8 @@ @media (min-width: 1200px) { .doc__fig--wide { width: min(1040px, calc(100vw - var(--rail-w, 0px) - 6rem)); + margin-left: 50%; + transform: translateX(-50%); } } From 3de959896acf43010363b1b54133fd29ecb7e233 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:11:36 -0700 Subject: [PATCH 06/37] marketing: centre rail and content in one fixed-width frame Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/styles/global.css | 40 +++++++++------------- apps/marketing/src/styles/variants/doc.css | 20 +++++------ 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index 5f93cedf0f..fc3f0e1e28 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1694,9 +1694,12 @@ body { } } -/* ── Sidebar ("rail") layout. Active when a rail option is visible. ── */ +/* ── Sidebar ("rail") layout. Active when a rail option is visible. ── + One centred frame (rail + content) of fixed max width, like a document + with a margin column. Content never exceeds its column. */ .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) { --rail-w: 280px; + --frame-w: 1240px; } .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail--b) { --rail-w: 240px; @@ -1709,23 +1712,27 @@ body { display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); align-items: start; + max-width: var(--frame-w); + margin-inline: auto; } .page__rail { position: sticky; top: 0; height: 100dvh; overflow: hidden auto; + border-right: 1px solid var(--color-rule); } - /* Content sits against the rail with a fixed gutter, like a document. */ .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body { - max-width: none; - padding-left: 0; + min-width: 0; + padding-inline: clamp(32px, 4vw, 64px); } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner { + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center { text-align: left; - margin-left: 0; } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .mx-auto { + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body [class*="max-w-"].mx-auto, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .mx-auto, + .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center .mx-auto { margin-left: 0; } .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .hero-cta, @@ -1736,21 +1743,6 @@ body { .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner h1 { font-size: clamp(2.6rem, 5vw, 4.4rem); } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center { - text-align: left; - } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center .mx-auto, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center.mx-auto { - margin-left: 0; - } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .pt-24, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body > section:first-child > .relative { - padding-top: 4.5rem; - } -} -@media (min-width: 1024px) { - /* In rail mode every section shares the hero's left edge, like a document. */ - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body [class*="max-w-"].mx-auto { - margin-left: 0; - } + /* The rail column has no page gutter of its own inside the frame; keep the + graph-paper hero texture behind the whole frame, not just the body. */ } diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index 2fc2be8e4d..67192a9673 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -9,11 +9,14 @@ } } @media (min-width: 1024px) { - /* Centre the column in the space beside the rail. */ + /* Fill the content column; the frame around rail + content is centred. */ .doc { - margin-inline: auto; - padding-inline: 2.5rem; - max-width: calc(720px + 5rem); + max-width: none; + padding-inline: 0; + } + .doc__intro, + .doc__section > :not(.doc__fig--wide) { + max-width: 720px; } } @@ -107,12 +110,9 @@ } /* The context demo is designed for a wide canvas; let it break out of the prose measure on large screens. */ -@media (min-width: 1200px) { - .doc__fig--wide { - width: min(1040px, calc(100vw - var(--rail-w, 0px) - 6rem)); - margin-left: 50%; - transform: translateX(-50%); - } +/* The context demo spans the full content column but never past it. */ +.doc__fig--wide { + max-width: 100%; } .doc__beats { From bc652c86c516d319c4d01efa624859747a0e8183 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:13:15 -0700 Subject: [PATCH 07/37] marketing: no graph-paper texture in rail mode Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/styles/global.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index fc3f0e1e28..d850f3aa87 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1746,3 +1746,11 @@ body { /* The rail column has no page gutter of its own inside the frame; keep the graph-paper hero texture behind the whole frame, not just the body. */ } + +/* The hero graph-paper texture is a landing-page device. In rail mode the + page reads as a document, so it goes: the texture would sit behind the + sidebar and the hairline frame and fight both. */ +main:has(.page > [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) > .pattern-bg, +main:has(.page > [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) > .pattern-bg + div { + display: none; +} From 1a5ea021a1fdd7a514f6c400b28829dbc033d719 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:17:17 -0700 Subject: [PATCH 08/37] marketing: hero variants for cloud, local, and self-hosted paths; gridline rail default Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 131 ++++++++++++++++ .../src/components/variants/rail-spy.ts | 9 ++ apps/marketing/src/pages/index.astro | 4 +- apps/marketing/src/styles/variants/doc.css | 148 ++++++++++++++++++ 4 files changed, 290 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index 39c767dfc5..eac1604156 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -17,6 +17,8 @@ const { gh } = Astro.props;
{/* ── Intro ── */} +
+ +
+
+

+ Executor is an MCP gateway. Your agent points at one endpoint and reaches every tool you connect. +

+

+ Claude Code, Cursor, Codex, or anything else that speaks MCP. Any tool from any + protocol. One place to add it, authenticate it, and decide what it is allowed to do. +

+

Run it hosted, on your machine, or on your own infrastructure. Same tools, same policies.

+ +
+ + Paste it into your agent and it walks you through setup. +
+
+
+ + +
{/* ── 01 Mission ── */}
@@ -186,3 +300,20 @@ const { gh } = Astro.props;

+ + diff --git a/apps/marketing/src/components/variants/rail-spy.ts b/apps/marketing/src/components/variants/rail-spy.ts index 1e7ff882c3..434906af12 100644 --- a/apps/marketing/src/components/variants/rail-spy.ts +++ b/apps/marketing/src/components/variants/rail-spy.ts @@ -46,6 +46,15 @@ export function initRailSpy(root: HTMLElement): void { const row = link.closest(".rail__toc-item") ?? link; row.style.display = shown ? "" : "none"; } + // Keep any mono index contiguous over the rows that remain. + let n = 0; + for (const { link } of pairs) { + const row = link.closest(".rail__toc-item") ?? link; + if (row.style.display === "none") continue; + n += 1; + const num = row.querySelector(".rail__num"); + if (num) num.textContent = String(n).padStart(2, "0"); + } }; refreshRows(); new MutationObserver(refreshRows).observe(document.body, { diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index 912d81d6e0..a056252e10 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -108,10 +108,10 @@ const stars = await getStars();
-
+ - +
+
+ +
+
+ Figure 1. Three clients reach six tools through one + MCP endpoint. Each tool keeps the protocol it came from. +
+
+ {/* ── 01 Mission ── */}

What we think

@@ -174,15 +184,6 @@ const { gh } = Astro.props; imported, so a GET and a DELETE are not the same thing, and it runs every call in a sandbox where a raw token never reaches the model.

-
-
- -
-
- Figure 1. Three clients reach six tools through one - MCP endpoint. Each tool keeps the protocol it came from. -
-
{/* ── 02 Context ── */} diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index 3b6575614b..a43a5d7313 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -339,3 +339,8 @@ padding: 0.55rem 0.75rem; color: var(--color-ink); } + +.doc__fig--hero { + margin-top: 2rem; + max-width: 720px; +} From 19125b174d79b1582b5354ed7fa09751744cf57d Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:29:15 -0700 Subject: [PATCH 10/37] marketing: settle on gridline rail and document body Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/pages/index.astro | 827 +-------------------------- apps/marketing/src/styles/global.css | 26 +- 2 files changed, 15 insertions(+), 838 deletions(-) diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index a056252e10..be1e53e25b 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -1,22 +1,11 @@ --- import Layout from "../layouts/Layout.astro"; -import { AnimatedBeamDemo } from "../components/animated-beam-demo"; -import { ContextBloatDemo } from "../components/context-bloat-demo"; // Imported so Vite emits it as a hashed build asset (served from /_astro/...); // the deploy pipeline does not pick up newly added /public files. -import ycBackedBy from "../assets/yc-backed-by.svg?url"; import { getStars, formatStars } from "../lib/github"; import { setupPrompt } from "../content/site-copy"; -import MissionA from "../components/variants/mission-a.astro"; -import MissionB from "../components/variants/mission-b.astro"; -import MissionC from "../components/variants/mission-c.astro"; -import ProofA from "../components/variants/proof-a.astro"; -import ProofB from "../components/variants/proof-b.astro"; -import ProofC from "../components/variants/proof-c.astro"; import AgentsFooter from "../components/variants/agents-footer.astro"; -import RailA from "../components/variants/rail-a.astro"; import RailB from "../components/variants/rail-b.astro"; -import RailC from "../components/variants/rail-c.astro"; import DocBody from "../components/variants/doc-body.astro"; const GH = "https://github.com/UsefulSoftwareCo/executor"; @@ -43,822 +32,10 @@ const stars = await getStars();
-
- - -
-
-
- -
+
-
- -
- -
-
+ {/* ─── FOOTER ─── */}
diff --git a/apps/marketing/src/styles/global.css b/apps/marketing/src/styles/global.css index d850f3aa87..9d82509580 100644 --- a/apps/marketing/src/styles/global.css +++ b/apps/marketing/src/styles/global.css @@ -1697,7 +1697,7 @@ body { /* ── Sidebar ("rail") layout. Active when a rail option is visible. ── One centred frame (rail + content) of fixed max width, like a document with a margin column. Content never exceeds its column. */ -.page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) { +.page:has(> .page__rail) { --rail-w: 280px; --frame-w: 1240px; } @@ -1708,7 +1708,7 @@ body { --rail-w: 220px; } @media (min-width: 1024px) { - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) { + .page:has(> .page__rail) { display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); align-items: start; @@ -1722,25 +1722,25 @@ body { overflow: hidden auto; border-right: 1px solid var(--color-rule); } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body { + .page:has(> .page__rail) .page__body { min-width: 0; padding-inline: clamp(32px, 4vw, 64px); } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center { + .page:has(> .page__rail) .page__body .hero-inner, + .page:has(> .page__rail) .page__body .text-center { text-align: left; } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body [class*="max-w-"].mx-auto, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .mx-auto, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .text-center .mx-auto { + .page:has(> .page__rail) .page__body [class*="max-w-"].mx-auto, + .page:has(> .page__rail) .page__body .hero-inner .mx-auto, + .page:has(> .page__rail) .page__body .text-center .mx-auto { margin-left: 0; } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .hero-cta, - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner .audience-pane { + .page:has(> .page__rail) .page__body .hero-inner .hero-cta, + .page:has(> .page__rail) .page__body .hero-inner .audience-pane { justify-content: flex-start; align-items: flex-start; } - .page:has(> [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) .page__body .hero-inner h1 { + .page:has(> .page__rail) .page__body .hero-inner h1 { font-size: clamp(2.6rem, 5vw, 4.4rem); } /* The rail column has no page gutter of its own inside the frame; keep the @@ -1750,7 +1750,7 @@ body { /* The hero graph-paper texture is a landing-page device. In rail mode the page reads as a document, so it goes: the texture would sit behind the sidebar and the hairline frame and fight both. */ -main:has(.page > [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) > .pattern-bg, -main:has(.page > [data-uidotsh-pick="Layout"] > [data-uidotsh-option]:not([hidden]) > .page__rail) > .pattern-bg + div { +main:has(.page > .page__rail) > .pattern-bg, +main:has(.page > .page__rail) > .pattern-bg + div { display: none; } From c392366a745b7d7353ac36a2168aedd43ac5a85b Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:05:50 -0700 Subject: [PATCH 11/37] marketing: five more hero variants Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 158 ++++++++++++++++++ apps/marketing/src/styles/variants/doc.css | 141 ++++++++++++++++ 2 files changed, 299 insertions(+) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index 04404e56c9..8b754e6fb5 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -153,6 +153,164 @@ const { gh } = Astro.props;

Works with Claude Code, Cursor, Codex, ChatGPT, and any MCP client.

+ + + + +
diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index a43a5d7313..090235d1ee 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -344,3 +344,144 @@ margin-top: 2rem; max-width: 720px; } + +/* ── More hero variants ── */ +.doc__lede--runs a { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--color-rule-strong); +} +.doc__lede--runs a:hover { + text-decoration-color: var(--color-ink); +} + +.doc__term { + font-family: var(--font-mono); + font-size: 12.5px; + border: 1px solid var(--color-rule); + border-radius: 8px; + background: var(--color-surface-2); + padding: 0.5rem 0; + margin-bottom: 1.1rem; +} +.doc__term-row { + display: grid; + grid-template-columns: 6.5rem 1fr auto; + gap: 1rem; + align-items: baseline; + padding: 0.45rem 1rem; +} +.doc__term-k { + color: var(--color-ink-3); +} +.doc__term-k::before { + content: "$ "; + color: var(--color-rule-strong); +} +.doc__term-v { + color: var(--color-ink); +} +a.doc__term-v { + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--color-rule-strong); +} +a.doc__term-v:hover { + text-decoration-color: var(--color-ink); +} +.doc__term-n { + color: var(--color-ink-3); + font-size: 11.5px; +} +@media (max-width: 719.98px) { + .doc__term-n { + display: none; + } +} + +.doc__rows { + display: grid; + margin-bottom: 1.1rem; +} +.doc__rows li { + display: grid; + grid-template-columns: 2.25rem 1fr auto; + gap: 0.75rem; + align-items: center; + padding: 0.8rem 0; + border-top: 1px solid var(--color-rule); +} +.doc__rows li:last-child { + border-bottom: 1px solid var(--color-rule); +} +.doc__rows-b { + display: flex; + flex-direction: column; + gap: 0.1rem; + font-size: 14.5px; + color: var(--color-ink-2); +} +.doc__rows-b strong { + color: var(--color-ink); + font-weight: 600; +} +.doc__rows-a { + padding: 0.5rem 0.9rem; + font-size: 13.5px; +} + +.doc__mono-links { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + font-family: var(--font-mono); + font-size: 12px; +} +.doc__mono-links a { + color: var(--color-ink-3); +} +.doc__mono-links a:hover { + color: var(--color-ink); +} + +.doc__icons { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + background: var(--color-rule); + border: 1px solid var(--color-rule); + border-radius: 10px; + overflow: hidden; +} +@media (min-width: 720px) { + .doc__icons { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +.doc__icon { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.2rem; + padding: 0.9rem 1rem; + background: var(--color-surface); + color: var(--color-ink-2); + transition: background 0.15s ease; +} +.doc__icon:hover { + background: var(--color-surface-2); +} +.doc__icon svg { + margin-bottom: 0.35rem; +} +.doc__icon span { + font-size: 14px; + font-weight: 600; + color: var(--color-ink); +} +.doc__icon small { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-ink-3); +} From 7ca39c71dfbcf24d1111de97cea70e086f0ca5ae Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:38:50 -0700 Subject: [PATCH 12/37] marketing: byline and signature linking to Rhys on X Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/assets/rhys-sullivan.jpg | Bin 0 -> 20561 bytes .../src/components/variants/doc-body.astro | 94 +++++++++++++++++- apps/marketing/src/styles/variants/doc.css | 59 +++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 apps/marketing/src/assets/rhys-sullivan.jpg diff --git a/apps/marketing/src/assets/rhys-sullivan.jpg b/apps/marketing/src/assets/rhys-sullivan.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a98109c473540925f0f16489dcd40cd97dc9848 GIT binary patch literal 20561 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 literal 0 HcmV?d00001 diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index 8b754e6fb5..78f099665d 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -8,6 +8,10 @@ import { AnimatedBeamDemo } from "../animated-beam-demo"; import { ContextBloatDemo } from "../context-bloat-demo"; import { setupPrompt } from "../../content/site-copy"; import "../../styles/variants/doc.css"; +// Avatar goes through Vite so it ships as a hashed asset; new /public files +// are not picked up by the deploy pipeline. +import rhysAvatar from "../../assets/rhys-sullivan.jpg"; +const X_URL = "https://x.com/RhysSullivan"; interface Props { gh: string; @@ -20,7 +24,16 @@ const { gh } = Astro.props;
+

Executor is an MCP gateway. Your agent points at one endpoint and reaches every tool you connect.

@@ -84,6 +106,15 @@ const { gh } = Astro.props;
-
+
@@ -507,7 +177,7 @@ const { gh } = Astro.props;
  • 04
    - Self-hosted. A Docker image or a Cloudflare Worker. + Self-hosted. A Docker image. Docs ↗
  • diff --git a/apps/marketing/src/components/variants/mission-a.astro b/apps/marketing/src/components/variants/mission-a.astro deleted file mode 100644 index 1b921dd8d8..0000000000 --- a/apps/marketing/src/components/variants/mission-a.astro +++ /dev/null @@ -1,100 +0,0 @@ ---- -// Mission, variant A: "Thesis". -// Three short paragraphs of prose with inline reference markers, and a -// numbered reference list at the foot of the section. Replaces the old -// one-sentence wedge. -import "../../styles/variants/mission.css"; ---- - -
    -
    -
    Mission
    - -
    -

    - Every agent client re-implements every integration. The same API gets wired up again for each new client, with its own auth, - its own schema handling, and its own failure modes. Credentials end up pasted - in five places. Nothing shares an idea of what a tool is allowed to do, so - each client guesses.1 -

    - -

    - Executor gives every tool one shape. A name, an input schema, an output - schema. It does not matter whether the tool came from an MCP server, an - OpenAPI spec, or a GraphQL API. Any agent calls any of them the same - way. Tools should belong to you, not to one client or one model - provider.2 -

    - -

    - The safe path has to be the easy path. Executor keeps the semantics it - imported: GET against DELETE for OpenAPI, destructiveHint for MCP, - mutations for GraphQL.3 Calls run in a sandbox. Credentials are attached host-side at call time, - so a raw token never reaches the sandbox, the agent, or the model.4 -

    -
    - -
    -
    References
    -
      -
    1. - [1] - Code execution with MCP Anthropic, 2025 -
    2. -
    3. - [2] - Code Mode Cloudflare -
    4. -
    5. - [3] - Tools, tool annotations Model Context Protocol, 2025-06-18 -
    6. -
    7. - [4] - Sandboxing and credentials - Executor docs -
    8. -
    -
    -
    -
    diff --git a/apps/marketing/src/components/variants/mission-b.astro b/apps/marketing/src/components/variants/mission-b.astro deleted file mode 100644 index 1bd56ae1df..0000000000 --- a/apps/marketing/src/components/variants/mission-b.astro +++ /dev/null @@ -1,106 +0,0 @@ ---- -// Mission, variant B: "Numbered claims". -// A short headline, then three hairline-separated rows. Each row is a mono -// index, a bold one-sentence claim, and two sentences of support. -import "../../styles/variants/mission.css"; ---- - -
    -
    -
    Mission
    -

    - Three things we think are - true. -

    - -
    -
    -
    001
    -
    -

    Tools should outlive the client.

    -

    - Today every agent client re-implements every integration, and the - same credentials get pasted in five places. Your tools should belong - to you, not to one client or one model provider.1 -

    -
    -
    - -
    -
    002
    -
    -

    One shape for every tool.

    -

    - An MCP server, an OpenAPI spec, and a GraphQL API all become a name, - an input schema, and an output schema. Any agent then calls any of - them the same way, on any surface.2 -

    -
    -
    - -
    -
    003
    -
    -

    The safe path is the easy path.

    -

    - Executor keeps the semantics it imported: GET against DELETE, - destructiveHint, GraphQL mutations.3 Calls run in a sandbox, and credentials are attached host-side at call - time, so a raw token never reaches the model.4 -

    -
    -
    -
    - - -
    -
    diff --git a/apps/marketing/src/components/variants/mission-c.astro b/apps/marketing/src/components/variants/mission-c.astro deleted file mode 100644 index b6d0657068..0000000000 --- a/apps/marketing/src/components/variants/mission-c.astro +++ /dev/null @@ -1,122 +0,0 @@ ---- -// Mission, variant C: "Two column with pull quotes". -// Tightened thesis prose on the left, two margin notes on the right. The -// notes column sits beside the prose from 900px up and stacks beneath it -// below that. Both quotes are verbatim from the linked sources. -import "../../styles/variants/mission.css"; ---- - -
    -
    -
    Mission
    - -
    -
    -

    - Every agent client re-implements every integration. The same API is wired up again for each client, with its own auth and - its own failure modes. Credentials get pasted in five places. No client - shares an idea of what a tool may do.1 -

    - -

    - Executor gives every tool one shape. A name, an input schema, an - output schema. MCP, OpenAPI, or GraphQL, it makes no difference: any - agent calls any of them the same way. Tools should belong to you, not - to one client or one model provider.2 -

    - -

    - The safe path has to be the easy path. Executor keeps the semantics it - imported: GET against DELETE, destructiveHint, GraphQL mutations.3 Calls run in a sandbox. Credentials are attached host-side, so a raw token - never reaches the model.4 -

    -
    - - -
    - - -
    -
    diff --git a/apps/marketing/src/components/variants/proof-a.astro b/apps/marketing/src/components/variants/proof-a.astro deleted file mode 100644 index 00bba9cca7..0000000000 --- a/apps/marketing/src/components/variants/proof-a.astro +++ /dev/null @@ -1,228 +0,0 @@ ---- -/** - * Proof section, variant A: "Tile grid". - * - * A 1px-gap grid of evidence tiles. Every tile is one real, checkable number - * with a mono caption, its source, a big tabular numeral, and a small - * grayscale visual. Nothing here is illustrative: each value links out to - * where it came from, or to the demo that produces it on the page. - */ -import "../../styles/variants/proof.css"; - -interface Props { - readonly stars: string | null; -} - -const { stars } = Astro.props; - -const GH = "https://github.com/UsefulSoftwareCo/executor"; -const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; - -// Context figures come from the interactive demo on this page, with GitHub, -// Stripe, Jira and Sentry connected. -const NAIVE_TOKENS = 278_800; -const EXEC_TOKENS = 1_044; -const NAIVE_TOOLS = 1_640; -const EXEC_TOOLS = 1; - -// The two pairs span three orders of magnitude, so the tile bars use a log -// scale (labelled as such) to stay readable. The hero variant shows the same -// pair linearly. -const logWidth = (value: number, max: number): string => - `${Math.max(2, (Math.log10(value + 1) / Math.log10(max + 1)) * 100).toFixed(1)}%`; - -const fmt = (n: number): string => n.toLocaleString("en-US"); ---- - -
    -
    -
    -
    In practice
    -

    Numbers you can check.

    -

    - No benchmark we ran on ourselves in private. Each figure below links to - the place it comes from: the demo on this page, the repo, or the - registry. -

    -
    - -
    - {/* Context tokens: the headline pair, on a log scale */} -
    -
    Context tokens
    -
    Four integrations connected
    -
    - {fmt(EXEC_TOKENS)}tok -
    -
    -
    -
    - Naive - - {fmt(NAIVE_TOKENS)} -
    -
    - Executor - - {fmt(EXEC_TOKENS)} -
    -
    -
    -
    - Log scale - Source ↗ -
    -
    - - {/* Tools exposed to the model */} -
    -
    Tools exposed
    -
    Measured in the demo below
    -
    - {EXEC_TOOLS}tool -
    -
    -
    -
    - Naive - - {fmt(NAIVE_TOOLS)} -
    -
    - Executor - - {fmt(EXEC_TOOLS)} -
    -
    -
    -
    - Log scale - Source ↗ -
    -
    - - {/* GitHub stars: only rendered when the page fetch succeeded */} - { - stars != null && ( -
    -
    GitHub stars
    -
    UsefulSoftwareCo / executor
    -
    {stars}
    -
    - -
    -
    - 305 forks - - Source ↗ - -
    -
    - ) - } - - {/* npm downloads */} -
    -
    npm downloads
    -
    Package: executor, last 30 days
    -
    - 70k/ 30d -
    -
    - -
    -
    - MIT licensed - Source ↗ -
    -
    - - {/* Free tier */} -
    -
    Free tier
    -
    Executions per month
    -
    100,000
    -
    - -
    -
    - 1 square = 10,000 - Source ↗ -
    -
    - - {/* Protocols */} -
    -
    Protocols supported
    -
    MCP, OpenAPI, GraphQL, Discovery
    -
    4
    -
    - -
    -
    - More on request - Source ↗ -
    -
    -
    -
    -
    diff --git a/apps/marketing/src/components/variants/proof-b.astro b/apps/marketing/src/components/variants/proof-b.astro deleted file mode 100644 index 032a6e50b8..0000000000 --- a/apps/marketing/src/components/variants/proof-b.astro +++ /dev/null @@ -1,232 +0,0 @@ ---- -/** - * Proof section, variant B: "Ledger". - * - * The same evidence as variant A, but set as one hairline-ruled spec sheet: - * caption, a thin proportional bar where there is a before/after, the value in - * right-aligned tabular numerals, and the source. It reads like a data sheet - * rather than a set of cards. - * - * Deliberately absent: an "independent benchmark" card. We do not have a - * third-party benchmark yet, so there is no social-proof slot to fill. The - * closing line points at the sources instead. - */ -import "../../styles/variants/proof.css"; - -interface Props { - readonly stars: string | null; -} - -const { stars } = Astro.props; - -const GH = "https://github.com/UsefulSoftwareCo/executor"; -const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; - -const NAIVE_TOKENS = 278_800; -const EXEC_TOKENS = 1_044; -const NAIVE_TOOLS = 1_640; -const EXEC_TOOLS = 1; - -// Three orders of magnitude between the pairs, so the row bars use a log -// scale. Each such row says so in its caption. -const logWidth = (value: number, max: number): string => - `${Math.max(2, (Math.log10(value + 1) / Math.log10(max + 1)) * 100).toFixed(1)}%`; - -const fmt = (n: number): string => n.toLocaleString("en-US"); ---- - -
    -
    -
    -
    In practice
    -

    Numbers you can check.

    -

    - The whole record, in one sheet. Two of these you can reproduce yourself - in the demo on this page. The rest are public counters. -

    -
    - -
    - {/* Context tokens */} -
    -
    - Context tokens - Four integrations, log scale -
    -
    -
    -
    - Naive - - {fmt(NAIVE_TOKENS)} -
    -
    - Executor - - {fmt(EXEC_TOKENS)} -
    -
    -
    -
    - {fmt(EXEC_TOKENS)}tok -
    - Source ↗ -
    - - {/* Tools exposed */} -
    -
    - Tools exposed - Four integrations, log scale -
    -
    -
    -
    - Naive - - {fmt(NAIVE_TOOLS)} -
    -
    - Executor - - {fmt(EXEC_TOOLS)} -
    -
    -
    -
    - {EXEC_TOOLS}tool -
    - Source ↗ -
    - - {/* GitHub stars: hidden when the fetch failed */} - { - stars != null && ( -
    -
    - GitHub stars - UsefulSoftwareCo / executor -
    -
    - 305 forks, MIT licensed -
    -
    {stars}
    - - Source ↗ - -
    - ) - } - - {/* npm downloads */} -
    -
    - npm downloads - Package: executor -
    -
    - Last 30 days, rounded -
    -
    70k/ 30d
    - Source ↗ -
    - - {/* Free tier */} -
    -
    - Free tier executions - Unlimited integrations -
    -
    - No card to start -
    -
    - 100,000/ mo -
    - Source ↗ -
    - - {/* Protocols */} -
    -
    - Protocols supported - One tool shape for all of them -
    -
    - MCP, OpenAPI, GraphQL, Google Discovery -
    -
    4
    - Source ↗ -
    - - {/* Ways to run it */} -
    -
    - Ways to run it - Same code paths, different packaging -
    -
    - Desktop app, CLI, Executor Cloud, self-hosted Docker or Cloudflare - Worker -
    -
    4
    - Source ↗ -
    - - {/* Secrets */} -
    -
    - Secrets in the sandbox - Injected host-side at call time -
    -
    - Tool calls run in an isolated JavaScript sandbox -
    -
    0
    - Source ↗ -
    -
    - -

    Every number above links to where it came from.

    -
    -
    diff --git a/apps/marketing/src/components/variants/proof-c.astro b/apps/marketing/src/components/variants/proof-c.astro deleted file mode 100644 index 0361e66a97..0000000000 --- a/apps/marketing/src/components/variants/proof-c.astro +++ /dev/null @@ -1,121 +0,0 @@ ---- -/** - * Proof section, variant C: "Before/after hero stat". - * - * One dominant figure carries the section: the prompt-size difference with - * four integrations connected, drawn as two linear bars so the ratio is the - * picture rather than a claim. Four smaller public counters sit beneath it. - */ -import "../../styles/variants/proof.css"; - -interface Props { - readonly stars: string | null; -} - -const { stars } = Astro.props; - -const GH = "https://github.com/UsefulSoftwareCo/executor"; -const NPM_SRC = "https://api.npmjs.org/downloads/point/last-month/executor"; - -const NAIVE_TOKENS = 278_800; -const EXEC_TOKENS = 1_044; -const NAIVE_TOOLS = 1_640; -const EXEC_TOOLS = 1; - -// Linear on purpose: at this ratio the second bar is a sliver, which is the -// honest picture. A minimum width in the CSS keeps it visible. -const multiplier = Math.round(NAIVE_TOKENS / EXEC_TOKENS); -const execWidth = `${((EXEC_TOKENS / NAIVE_TOKENS) * 100).toFixed(2)}%`; - -const fmt = (n: number): string => n.toLocaleString("en-US"); ---- - -
    -
    -
    -
    In practice
    -

    Numbers you can check.

    -

    - Connect GitHub, Stripe, Jira and Sentry. A naive setup hands the model - every tool definition up front. Executor hands it one. -

    -
    - -
    -
    -
    -
    Smaller prompt
    -
    {multiplier}×
    -
    - -
    -
    -
    - Without Executor - {fmt(NAIVE_TOKENS)} tok · {fmt(NAIVE_TOOLS)} tools -
    -
    - -
    -
    - -
    -
    - With Executor - {fmt(EXEC_TOKENS)} tok · {EXEC_TOOLS} tool -
    -
    - -
    -
    -
    -
    - -
    - Figure 2. Prompt size with four integrations connected. Reproduce it in - the demo above. -
    -
    - - -
    -
    diff --git a/apps/marketing/src/components/variants/rail-a.astro b/apps/marketing/src/components/variants/rail-a.astro deleted file mode 100644 index f2ff57b10e..0000000000 --- a/apps/marketing/src/components/variants/rail-a.astro +++ /dev/null @@ -1,100 +0,0 @@ ---- -// Rail, variant A: "Paper rail". -// Full left sidebar: wordmark, primary nav with an active tab bar, the star -// pill and the primary CTA, then a ticked "On this page" index. The page shell -// owns the column and its sticky behaviour. -import "../../styles/variants/rail.css"; - -interface Props { - stars: string | null; -} -const { stars } = Astro.props; - -const GH = "https://github.com/UsefulSoftwareCo/executor"; -const SIGN_IN = "/api/auth/login"; - -const sections = [ - { id: "mission", label: "What we think" }, - { id: "context", label: "Context" }, - { id: "proof", label: "In practice" }, - { id: "capabilities", label: "What you get" }, - { id: "safety", label: "Safety" }, - { id: "install", label: "Run it" }, - { id: "faq", label: "FAQ" }, - { id: "pricing", label: "Pricing" }, - { id: "writing", label: "Writing" }, - { id: "about", label: "About" }, -]; ---- - - - - diff --git a/apps/marketing/src/components/variants/rail-b.astro b/apps/marketing/src/components/variants/rail-b.astro index cebdc8f1f0..15a287a687 100644 --- a/apps/marketing/src/components/variants/rail-b.astro +++ b/apps/marketing/src/components/variants/rail-b.astro @@ -16,11 +16,8 @@ const SIGN_IN = "/api/auth/login"; const sections = [ { id: "mission", label: "What we think" }, { id: "context", label: "Context" }, - { id: "proof", label: "In practice" }, - { id: "capabilities", label: "What you get" }, { id: "safety", label: "Safety" }, { id: "install", label: "Run it" }, - { id: "faq", label: "FAQ" }, { id: "pricing", label: "Pricing" }, { id: "writing", label: "Writing" }, { id: "about", label: "About" }, diff --git a/apps/marketing/src/components/variants/rail-c.astro b/apps/marketing/src/components/variants/rail-c.astro deleted file mode 100644 index c4dbe838ca..0000000000 --- a/apps/marketing/src/components/variants/rail-c.astro +++ /dev/null @@ -1,65 +0,0 @@ ---- -// Rail, variant C: "Minimal rail". -// Five section links carry the whole rail, so they double as the index. The -// secondary links sit below a hairline. No CTA here: the hero already has one. -import "../../styles/variants/rail.css"; - -interface Props { - stars: string | null; -} -const { stars } = Astro.props; - -const GH = "https://github.com/UsefulSoftwareCo/executor"; -const SIGN_IN = "/api/auth/login"; - -const sections = [ - { id: "mission", label: "What we think" }, - { id: "context", label: "Context" }, - { id: "proof", label: "In practice" }, - { id: "capabilities", label: "What you get" }, - { id: "safety", label: "Safety" }, - { id: "install", label: "Run it" }, - { id: "faq", label: "FAQ" }, - { id: "pricing", label: "Pricing" }, - { id: "writing", label: "Writing" }, - { id: "about", label: "About" }, -]; ---- - - - - diff --git a/apps/marketing/src/components/variants/rail-spy.ts b/apps/marketing/src/components/variants/rail-spy.ts index 434906af12..fba86febe6 100644 --- a/apps/marketing/src/components/variants/rail-spy.ts +++ b/apps/marketing/src/components/variants/rail-spy.ts @@ -5,10 +5,8 @@ // with `aria-current="true"`, and mirrors the id onto the aside as // `data-active-section` so CSS can react without extra classes. // -// Rails can be mounted inside a hidden picker wrapper, and the picker can swap -// which one is visible at any time. So: bind unconditionally (an observer on a -// hidden subtree is cheap and harmless), tolerate missing sections, and stay -// idempotent if the initializer runs again on the same element. +// Tolerates missing sections and stays idempotent if the initializer runs +// again on the same element. const BOUND = "railSpyBound"; @@ -25,10 +23,8 @@ export function initRailSpy(root: HTMLElement): void { const pairs: Array<{ id: string; link: HTMLAnchorElement }> = []; for (const link of links) { const id = decodeURIComponent(link.getAttribute("href")!.slice(1)); - // Picker wrappers can hold several copies of a section under one id; only - // the visible copy ever intersects, so observe all of them. - const sections = id ? document.querySelectorAll(`[id="${CSS.escape(id)}"]`) : []; - if (sections.length === 0) { + const section = id ? document.getElementById(id) : null; + if (section == null) { const row = link.closest(".rail__toc-item") ?? link; row.style.display = "none"; continue; @@ -37,32 +33,6 @@ export function initRailSpy(root: HTMLElement): void { } if (pairs.length === 0) return; - // A target that only exists inside a hidden picker wrapper is not on the - // page the reader sees; hide its row, and re-check when wrappers toggle. - const refreshRows = (): void => { - for (const { id, link } of pairs) { - const targets = document.querySelectorAll(`[id="${CSS.escape(id)}"]`); - const shown = Array.from(targets).some((t) => t.closest("[hidden]") == null); - const row = link.closest(".rail__toc-item") ?? link; - row.style.display = shown ? "" : "none"; - } - // Keep any mono index contiguous over the rows that remain. - let n = 0; - for (const { link } of pairs) { - const row = link.closest(".rail__toc-item") ?? link; - if (row.style.display === "none") continue; - n += 1; - const num = row.querySelector(".rail__num"); - if (num) num.textContent = String(n).padStart(2, "0"); - } - }; - refreshRows(); - new MutationObserver(refreshRows).observe(document.body, { - attributes: true, - attributeFilter: ["hidden"], - subtree: true, - }); - const order = pairs.map((p) => p.id); const visible = new Set(); @@ -99,8 +69,7 @@ export function initRailSpy(root: HTMLElement): void { ); for (const { id } of pairs) { - for (const section of document.querySelectorAll(`[id="${CSS.escape(id)}"]`)) { - observer.observe(section); - } + const section = document.getElementById(id); + if (section != null) observer.observe(section); } } diff --git a/apps/marketing/src/content/site-copy.ts b/apps/marketing/src/content/site-copy.ts index d572b15682..28bbdcfe45 100644 --- a/apps/marketing/src/content/site-copy.ts +++ b/apps/marketing/src/content/site-copy.ts @@ -27,11 +27,11 @@ Both run a local HTTP server as a background service that any MCP client can con Hosted (use it from multiple agents, including cloud ones, with nothing running locally): - Executor Cloud: hosted, generous free tier, sign in and start immediately. -- Self-hosted: a Docker image or a Cloudflare Worker. +- Self-hosted: a Docker image. How to think about it: - Want all your data on your own machine? Go local: the desktop app for a regular environment, the CLI for a headless one. -- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker or Cloudflare versions give you full control. +- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker version gives you full control. Terms you'll come across: - Integration: anything you add (an MCP server, an OpenAPI spec, a GraphQL API). diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index be1e53e25b..6000f5db44 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -4,7 +4,6 @@ import Layout from "../layouts/Layout.astro"; // the deploy pipeline does not pick up newly added /public files. import { getStars, formatStars } from "../lib/github"; import { setupPrompt } from "../content/site-copy"; -import AgentsFooter from "../components/variants/agents-footer.astro"; import RailB from "../components/variants/rail-b.astro"; import DocBody from "../components/variants/doc-body.astro"; @@ -59,47 +58,9 @@ const stars = await getStars(); Terms
    -
    -
    - -

    - - @@ -155,115 +116,6 @@ const stars = await getStars(); } - - - - diff --git a/apps/marketing/src/content/testimonials.ts b/apps/marketing/src/content/testimonials.ts index fb58d9ecf6..da0cee113d 100644 --- a/apps/marketing/src/content/testimonials.ts +++ b/apps/marketing/src/content/testimonials.ts @@ -2,7 +2,8 @@ // with @-mentions and links reduced to plain words so a card reads on its own. // Avatars live in src/assets/pfps/. and ship as hashed assets. // -// Order is the display order; the marquee rows draw from this list in turn. +// Order is the display order; the marquee rows draw from this list in turn, so +// the first twelve are what a reader sees before the rows start to move. export interface Testimonial { readonly handle: string; @@ -24,18 +25,66 @@ export const testimonials: readonly Testimonial[] = [ id: "2099995262802641129", text: "For the last 3 months I've been telling all my clients to move everything to MCPs and executor.sh.", }, + { + handle: "cramforce", + name: "Malte Ubl", + id: "2042626213697720632", + text: "I've been working on integrating Rhys's excellent `executor` package into just-bash.", + }, + { + handle: "aidenybai", + name: "Aiden Bai", + id: "2052907020202979362", + text: "executor is very good software", + }, { handle: "chribjel", name: "Christoffer Bjelke", id: "2092939357980123545", text: "update: it *is* the perfect wrapper. now executor.sh is my only mcp for all my agents, and i just configure my other connected services through executor. all integrations can have multiple connections, so i can have personal/work accounts for every integration. so so nice, literally perfect", }, + { + handle: "ethanniser", + name: "Ethan Niser", + id: "2094632205570207834", + text: "I dont think I've ever oauthed so much in my life until AI. connections, connections, connections. s/o executor.sh", + }, { handle: "ThomsenDrake", name: "Drake Thomsen", id: "2093758541798506783", text: "Moving my connectors onto executor.sh is such a refreshing experience. I now only have to configure a single connector when I'm setting up a new agent instead of 15 different OAuths or API keys", }, + { + handle: "NathanFlurry", + name: "Nathan Flurry", + id: "2078663475782685025", + text: "really digging executor + browserbase for unifying my integrations. i use agents in a lot of places: linux rig over ssh, amp orbs for bg agents, claude/chatgpt on mobile. this setup lets me sign in once (browser or mcp) and access it everywhere", + }, + { + handle: "samhogan", + name: "Sam Hogan", + id: "2094813654202089970", + text: "My favorite features: Quick set-up. Easily share MCPs between agent harnesses with centralized authentication and access control. Auth once, use everywhere. Codemode execution by default", + }, + { + handle: "DanielLockyer", + name: "Daniel Lockyer", + id: "2084357678105563583", + text: "I've been using Executor since this mini-pitch and honestly it's great. I just configure my MCPs/OpenAPIs in one place and all my clients can use the connections. I've also used the blocklist feature to remove dangerous actions from some MCPs, very nice. Can recommend!", + }, + { + handle: "JosXa_", + name: "JosXa", + id: "2054494157096329504", + text: "I compared dozens of MCP aggregators and we finally have a winner. Executor is going to become a standard, I can smell it", + }, + { + handle: "davis7", + name: "Ben Davis", + id: "2036226001877999868", + text: "Really like what Rhys is working on with executor. I think it or something like it is probably the future", + }, { handle: "BenceRedmond", name: "Bence Redmond", @@ -66,12 +115,6 @@ export const testimonials: readonly Testimonial[] = [ id: "2069516381381599593", text: "I was thinking about building some custom Effect-based MCPs like you did with YNAB, until I realized that Executor already did everything I wanted. It's been incredibly useful.", }, - { - handle: "JosXa_", - name: "JosXa", - id: "2054494157096329504", - text: "I compared dozens of MCP aggregators and we finally have a winner. Executor is going to become a standard, I can smell it", - }, { handle: "shuv1337", name: "shuv", @@ -84,18 +127,6 @@ export const testimonials: readonly Testimonial[] = [ id: "2090812289427386851", text: "very much enjoying Executor. configure different connections for the same MCP: I have a work and a personal Cloudflare account so I don't need to re-auth into different ones. manage all the connections between different clients. 1Password to manage secrets instead of storing them in plain text. really well done", }, - { - handle: "NathanFlurry", - name: "Nathan Flurry", - id: "2078663475782685025", - text: "really digging executor + browserbase for unifying my integrations. i use agents in a lot of places: linux rig over ssh, amp orbs for bg agents, claude/chatgpt on mobile. this setup lets me sign in once (browser or mcp) and access it everywhere", - }, - { - handle: "cramforce", - name: "Malte Ubl", - id: "2042626213697720632", - text: "I've been working on integrating Rhys's excellent `executor` package into just-bash.", - }, { handle: "kr0der", name: "Anthony Kroeger", @@ -228,12 +259,6 @@ export const testimonials: readonly Testimonial[] = [ id: "2082161772514160818", text: "Executor makes it so incredibly easy for end users", }, - { - handle: "aidenybai", - name: "Aiden Bai", - id: "2052907020202979362", - text: "executor is very good software", - }, { handle: "alexrigler", name: "Alex Rigler", @@ -276,12 +301,6 @@ export const testimonials: readonly Testimonial[] = [ id: "2083113232080724033", text: "all I had to do is to add it as integration to my executor, and now I have access to it as mcp without deploying any MCP", }, - { - handle: "samhogan", - name: "Sam Hogan", - id: "2094813654202089970", - text: "My favorite features: Quick set-up. Easily share MCPs between agent harnesses with centralized authentication and access control. Auth once, use everywhere. Codemode execution by default", - }, { handle: "UltraLinx", name: "Oliur", @@ -354,12 +373,6 @@ export const testimonials: readonly Testimonial[] = [ id: "2097672856477839558", text: "What are we doing people?! Just use executor.sh or find an alternative.", }, - { - handle: "ethanniser", - name: "Ethan Niser", - id: "2094632205570207834", - text: "I dont think I've ever oauthed so much in my life until AI. connections, connections, connections. s/o executor.sh", - }, { handle: "aidansunbury", name: "Aidan Sunbury", @@ -372,16 +385,4 @@ export const testimonials: readonly Testimonial[] = [ id: "2098769030735864132", text: "Finally. This is a great idea", }, - { - handle: "DanielLockyer", - name: "Daniel Lockyer", - id: "2084357678105563583", - text: "I've been using Executor since this mini-pitch and honestly it's great. I just configure my MCPs/OpenAPIs in one place and all my clients can use the connections. I've also used the blocklist feature to remove dangerous actions from some MCPs, very nice. Can recommend!", - }, - { - handle: "davis7", - name: "Ben Davis", - id: "2036226001877999868", - text: "Really like what Rhys is working on with executor. I think it or something like it is probably the future", - }, ]; diff --git a/apps/marketing/src/styles/variants/testimonials.css b/apps/marketing/src/styles/variants/testimonials.css index 2565a5c617..23ce68c877 100644 --- a/apps/marketing/src/styles/variants/testimonials.css +++ b/apps/marketing/src/styles/variants/testimonials.css @@ -26,11 +26,18 @@ gap: 12px; flex-shrink: 0; animation: testi-drift var(--testi-duration) linear infinite; + /* Hold still until the section has been seen once, so the first cards a + reader meets are the ones at the head of the list, not wherever the + loop happened to be. */ + animation-play-state: paused; +} +.testi[data-seen] .testi__track { + animation-play-state: running; } .testi__row[data-row="1"] .testi__track { animation-direction: reverse; } -.testi:hover .testi__track { +.testi[data-seen]:hover .testi__track { animation-play-state: paused; } @keyframes testi-drift { From e77ee44e982d11e65d9cde79991a95a813e03af4 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:52:23 -0700 Subject: [PATCH 23/37] marketing: slower testimonial drift Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/styles/variants/testimonials.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/marketing/src/styles/variants/testimonials.css b/apps/marketing/src/styles/variants/testimonials.css index 23ce68c877..0dda34bc36 100644 --- a/apps/marketing/src/styles/variants/testimonials.css +++ b/apps/marketing/src/styles/variants/testimonials.css @@ -13,13 +13,13 @@ display: flex; gap: 12px; overflow: hidden; - --testi-duration: 140s; + --testi-duration: 260s; } .testi__row[data-row="1"] { - --testi-duration: 165s; + --testi-duration: 300s; } .testi__row[data-row="2"] { - --testi-duration: 150s; + --testi-duration: 280s; } .testi__track { display: flex; From 8e1729e553ced3b5f85c28f8023e126d8c3f24cf Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:53:04 -0700 Subject: [PATCH 24/37] marketing: testimonial intro line Co-Authored-By: Claude Fable 5.1 --- apps/marketing/src/components/variants/doc-body.astro | 2 +- apps/marketing/src/pages/index.md.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index 185079e742..dc7d58cad5 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -115,7 +115,7 @@ const { gh } = Astro.props; {/* ── What people say ── */}

    What people say

    -

    Posts about Executor on X. Each card links to the original.

    +

    Mostly that they stopped copying API keys into five different agents.

    diff --git a/apps/marketing/src/pages/index.md.ts b/apps/marketing/src/pages/index.md.ts index 638cb33e49..ad94dd9bd2 100644 --- a/apps/marketing/src/pages/index.md.ts +++ b/apps/marketing/src/pages/index.md.ts @@ -134,7 +134,7 @@ ${pricingLines.join("\n\n")} ## What people say -Public posts about Executor on X. Each line links to the original. +Mostly that they stopped copying API keys into five different agents. ${testimonials.map((t) => `- ${t.name} (@${t.handle}): "${t.text}" https://x.com/${t.handle}/status/${t.id}`).join("\n")} From 5b096a08c494f67b498f2857ac32a4d9290c1593 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:56:04 -0700 Subject: [PATCH 25/37] marketing: add yours card in testimonials Co-Authored-By: Claude Fable 5.1 --- .../components/variants/testimonials.astro | 23 ++++++++++++++ .../src/styles/variants/testimonials.css | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/apps/marketing/src/components/variants/testimonials.astro b/apps/marketing/src/components/variants/testimonials.astro index b37fa77217..debaf1f363 100644 --- a/apps/marketing/src/components/variants/testimonials.astro +++ b/apps/marketing/src/components/variants/testimonials.astro @@ -23,6 +23,11 @@ const avatarFor = (handle: string): string | null => { const ROWS = 3; const rows: (typeof testimonials)[number][][] = Array.from({ length: ROWS }, () => []); testimonials.forEach((t, i) => rows[i % ROWS]!.push(t)); + +// "Add yours" opens X's composer with a draft the reader can edit or delete. +const composeUrl = `https://x.com/intent/post?text=${encodeURIComponent( + "I've been using @executorsh to ", +)}`; ---
    @@ -32,6 +37,24 @@ testimonials.forEach((t, i) => rows[i % ROWS]!.push(t)); {/* Two copies of the row make the marquee loop seamless. */} {[0, 1].map((copy) => (
    + {r === 1 ? ( + + + + Add yours + on X + + + Using Executor? Post about it and tag @executorsh. We read every one. + + + ) : null} {row.map((t) => { const src = avatarFor(t.handle); return ( diff --git a/apps/marketing/src/styles/variants/testimonials.css b/apps/marketing/src/styles/variants/testimonials.css index 0dda34bc36..d19afd2591 100644 --- a/apps/marketing/src/styles/variants/testimonials.css +++ b/apps/marketing/src/styles/variants/testimonials.css @@ -105,6 +105,36 @@ overflow: hidden; } +/* "Add yours": an empty-looking card at the head of the middle row. */ +.testi__card--add { + border-style: dashed; + border-color: var(--color-rule-strong); + background: transparent; +} +.testi__card--add:hover { + border-style: solid; + border-color: var(--color-ink); + background: var(--color-surface); +} +.testi__add-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid var(--color-rule-strong); + font-family: var(--font-mono); + font-size: 14px; + line-height: 1; + color: var(--color-ink-2); + flex-shrink: 0; +} +.testi__card--add:hover .testi__add-mark { + border-color: var(--color-ink); + color: var(--color-ink); +} + /* Reduced motion: no drift, one copy, wrap into a grid. */ @media (prefers-reduced-motion: reduce) { .testi { From a2c352895d0d8123a640412ec673721780b27fcc Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:02:52 -0700 Subject: [PATCH 26/37] marketing: consistent rail buttons; safety, run-it, and about copy from user feedback Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 43 +++++++++++------ .../src/components/variants/rail-b.astro | 4 +- apps/marketing/src/pages/index.md.ts | 7 +++ apps/marketing/src/styles/variants/rail.css | 48 ++++++++++++------- 4 files changed, 68 insertions(+), 34 deletions(-) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index dc7d58cad5..3532cffccc 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -112,13 +112,6 @@ const { gh } = Astro.props;
    - {/* ── What people say ── */} -
    -

    What people say

    -

    Mostly that they stopped copying API keys into five different agents.

    - -
    - {/* ── 03 Safety ── */}

    Safe by default

    @@ -141,8 +134,18 @@ const { gh } = Astro.props;
    Set up once, whole team has it.
    - Per-user credentials and shared ones. New teammates get the catalog on day one, - with the same policies. + Admins add workspace connections everyone shares. Individuals add their own. + New teammates get the catalog on day one, with the same policies, and you can + block a tool for the whole workspace in one click. +
    +
    +
    +
    Open source, so you can check.
    +
    + Cloud stores credentials in WorkOS Vault. Local and self-hosted keep them on + your machine, or in 1Password. The whole thing is on GitHub if you would + rather read the code than take our word for it. + Source ↗
    @@ -152,7 +155,8 @@ const { gh } = Astro.props;

    Run it where you want

    - All forms expose the same tools, packaged differently. + All forms expose the same tools, packaged differently. Cloud is the one we keep + updated for you. The rest you run and update yourself.

    1. @@ -185,6 +189,17 @@ const { gh } = Astro.props;
    +

    + Not an agent? Cloud also has an HTTP API with user and workspace keys, so a script + or a service can call the same tools with the same policies. +

    +
    + + {/* ── What people say ── */} +
    +

    What people say

    +

    Mostly that they stopped copying API keys into five different agents.

    +
    {/* ── 05 Pricing ── */} @@ -207,10 +222,10 @@ const { gh } = Astro.props;

    About

    - We are a small team backed by Y Combinator. We built Executor because we wanted our - own agents to reach our company's resources in a way that was not scary. Most setups - make you choose between locked down and useless, or wide open and risky. We wanted a - third option. + Executor is built by one person, backed by Y Combinator, with a lot of help from + the people above. It started because I wanted my own agents to reach my accounts in + a way that was not scary. Most setups make you choose between locked down and + useless, or wide open and risky. I wanted a third option.

    diff --git a/apps/marketing/src/components/variants/rail-b.astro b/apps/marketing/src/components/variants/rail-b.astro index 8fa4785e81..20a0c66970 100644 --- a/apps/marketing/src/components/variants/rail-b.astro +++ b/apps/marketing/src/components/variants/rail-b.astro @@ -20,9 +20,9 @@ const SIGN_IN = "/api/auth/login"; const sections = [ { id: "mission", label: "What we think" }, { id: "context", label: "Context" }, - { id: "people", label: "What people say" }, { id: "safety", label: "Safety" }, { id: "install", label: "Run it" }, + { id: "people", label: "What people say" }, { id: "pricing", label: "Pricing" }, { id: "writing", label: "Writing" }, { id: "about", label: "About" }, @@ -65,7 +65,7 @@ const pad = (index: number) => String(index + 1).padStart(2, "0"); d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z" > - star on github + Star on GitHub {stars != null && {stars}}
    1. Get started. +
    2. +
    @@ -37,8 +84,11 @@ const { gh } = Astro.props;
    -

    Hosted Executor. Sign in and point your agent at one URL. Free for up to three members and 100,000 executions a month.

    +

    Sign in, point your agent at one URL, done. Free for up to three people and 100,000 executions a month.

    Start on Cloud
    -

    Works with Claude Code, Cursor, Codex, ChatGPT, and any MCP client.

    @@ -78,20 +130,20 @@ const { gh } = Astro.props;

    What we think

    - Every agent client re-implements every integration. The same API gets wired up again - for each new tool, with its own auth and its own failure modes. Credentials end up - pasted in five places, and nothing shares an idea of what a tool is allowed to do. + Every agent wants its own copy of every integration. You set up GitHub in Claude + Code, then again in Cursor, then again in Codex. Same OAuth, same API key pasted in + five places, and none of them agree on what a tool is allowed to do.

    - We think tools should belong to you, not to one client or one model provider. So - Executor gives every tool one shape: a name, an input schema, an output schema. An - MCP server, an OpenAPI spec, and a GraphQL API all end up the same, and any agent - calls any of them the same way. + Your tools should belong to you, not to whichever agent you opened today. Executor + turns everything into one shape: a name, an input, an output. An MCP server, an + OpenAPI spec, and a GraphQL API all look the same to the agent, so you connect once + and every agent gets it.

    - We also think the safe path has to be the easy path. Executor keeps the semantics it - imported, so a GET and a DELETE are not the same thing, and it runs every call in a - sandbox where a raw token never reaches the model. + And the safe way has to be the easy way, or nobody does it. Executor knows a GET + from a DELETE, asks before the scary ones, and runs everything in a sandbox where + the model never sees a raw token.

    @@ -99,9 +151,9 @@ const { gh } = Astro.props;

    Thousands of tools, one in the prompt

    - Executor shows the model a single tool. It searches your catalog and loads a tool's - schema only when the code calls it, so connecting more services does not grow the - prompt. Toggle the services below to see the difference. + The model only ever sees one tool. Executor looks up what it needs when the code + asks for it, so you can connect fifty services and the prompt stays the same size. + Toggle a few below and watch the numbers.

    diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index 1e4d03cc28..cf188ee96c 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -544,3 +544,144 @@ a.doc__term-v:hover { .doc__sig:hover .doc__sig-b span { color: var(--color-ink); } + +/* ── Intro steps with hover reveals ── */ +.doc__steps { + display: grid; + gap: 0.55rem; + margin: 0.25rem 0 1.5rem; + font-size: 17px; + line-height: 1.5; + color: var(--color-ink); +} +.doc__steps li { + display: grid; + grid-template-columns: 2rem 1fr; + gap: 0.25rem; + align-items: baseline; +} +.doc__steps a { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 4px; + text-decoration-color: var(--color-rule-strong); +} +.doc__steps a:hover { + text-decoration-color: var(--color-ink); +} +.doc__pop { + position: relative; + display: inline-block; + outline: none; +} +.doc__pop-word { + font-weight: 600; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 4px; + text-decoration-color: var(--color-ink-3); + cursor: default; +} +.doc__pop:hover .doc__pop-word, +.doc__pop:focus-visible .doc__pop-word { + text-decoration-style: solid; + text-decoration-color: var(--color-ink); +} +.doc__pop-panel { + position: absolute; + left: 0; + top: calc(100% + 8px); + z-index: 20; + display: flex; + flex-wrap: wrap; + gap: 6px; + width: max-content; + max-width: 420px; + padding: 8px; + border: 1px solid var(--color-rule); + border-radius: 10px; + background: var(--color-surface); + box-shadow: + 0 1px 2px rgba(13, 13, 16, 0.04), + 0 12px 32px -12px rgba(13, 13, 16, 0.18); + opacity: 0; + pointer-events: none; + transform: translateY(-4px); + transition: + opacity 0.15s ease, + transform 0.15s ease; +} +.doc__pop:hover .doc__pop-panel, +.doc__pop:focus-within .doc__pop-panel { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} +.doc__pop-tile, +.doc__pop-pill { + opacity: 0; + transform: scale(0.6); + transition: + opacity 0.2s ease, + transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: calc(var(--i, 0) * 28ms); +} +.doc__pop:hover .doc__pop-tile, +.doc__pop:hover .doc__pop-pill, +.doc__pop:focus-within .doc__pop-tile, +.doc__pop:focus-within .doc__pop-pill { + opacity: 1; + transform: scale(1); +} +.doc__pop-tile { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: 1px solid var(--color-rule); + border-radius: 8px; + background: var(--color-surface); +} +.doc__pop-tile img { + width: 18px; + height: 18px; + object-fit: contain; +} +.doc__pop-pill { + display: inline-flex; + align-items: center; + height: 34px; + padding: 0 10px; + border: 1px solid var(--color-rule); + border-radius: 8px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--color-ink-2); + white-space: nowrap; +} +@media (prefers-reduced-motion: reduce) { + .doc__pop-panel, + .doc__pop-tile, + .doc__pop-pill { + transition: none; + } +} + +/* Agent marks on the copy button, so it reads as "for your agent". */ +.doc__copy-logos { + display: inline-flex; + margin-right: 0.35rem; +} +.doc__copy-logos img { + width: 16px; + height: 16px; + padding: 2px; + border-radius: 999px; + background: var(--color-surface); + box-shadow: 0 0 0 1px var(--color-rule); + object-fit: contain; +} +.doc__copy-logos img + img { + margin-left: -5px; +} From 4884594c955942c9c2700ab0cf11a0d6f73f23c9 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:48:00 -0700 Subject: [PATCH 29/37] marketing: platform-specific download buttons; drop self-referencing links Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 72 +++++++++++++++++-- apps/marketing/src/styles/variants/doc.css | 36 ++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index 94ed7599d5..ad6ba3805c 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -98,7 +98,16 @@ const { gh } = Astro.props;
    @@ -308,3 +326,49 @@ const { gh } = Astro.props; }); } + + diff --git a/apps/marketing/src/styles/variants/doc.css b/apps/marketing/src/styles/variants/doc.css index cf188ee96c..d66c6fb459 100644 --- a/apps/marketing/src/styles/variants/doc.css +++ b/apps/marketing/src/styles/variants/doc.css @@ -685,3 +685,39 @@ a.doc__term-v:hover { .doc__copy-logos img + img { margin-left: -5px; } + +/* ── Platform download ── */ +.doc__dl { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; +} +.doc__dl-link { + color: var(--color-ink); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--color-rule-strong); +} +.doc__dl-link:hover { + text-decoration-color: var(--color-ink); +} +.doc__dl-other > summary { + font-size: 13px; +} +.doc__dl .download-other-list { + left: 0; + transform: none; +} +.doc__ways .doc__dl { + margin-left: 0.4rem; +} +.doc__ways .doc__dl a { + margin-left: 0; +} +/* Explicit, since utility classes added at runtime are not guaranteed to be + in the build. */ +.doc__dl > .hidden, +.doc__dl-other.hidden { + display: none; +} From 542f6b9993c1c87b3517ff2f5307ad81f2c7f310 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:51:26 -0700 Subject: [PATCH 30/37] marketing: tab panes share one cell so switching never shifts layout Co-Authored-By: Claude Fable 5.1 --- .../src/components/variants/doc-body.astro | 11 ++++++++--- apps/marketing/src/styles/variants/doc.css | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/marketing/src/components/variants/doc-body.astro b/apps/marketing/src/components/variants/doc-body.astro index ad6ba3805c..a572a4967e 100644 --- a/apps/marketing/src/components/variants/doc-body.astro +++ b/apps/marketing/src/components/variants/doc-body.astro @@ -96,7 +96,7 @@ const { gh } = Astro.props; -