diff --git a/app/hermes/page.tsx b/app/hermes/page.tsx new file mode 100644 index 0000000..40aaaea --- /dev/null +++ b/app/hermes/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; + +const steps = ["Provider webhook", "clawproxy route", "Hermes agent run"]; +const snippet = `# 1. Create a clawproxy route in the dashboard +# Provider URL: https://clawproxy.io/api/ingress// + +# 2. Install the Hermes gateway plugin +hermes plugins install ericdahl-dev/clawproxy-hermes + +# 3. Add these to ~/.hermes/.env (or your profile's .env) +CLAWPROXY_NODE_TOKEN=cpn_your_node_token +CLAWPROXY_HERMES_WEBHOOK_SECRET= + +# 4. Restart the gateway, then check delivery +hermes gateway status`; + +export const dynamic = "force-dynamic"; + +export default function HermesLandingPage() { + return ( +
+
+
+
+ + clawproxy + Webhook relay for Hermes agent workflows + + +
+
+
+

Webhook relay for Hermes agent workflows

+

Route webhooks into private Hermes runs.

+

Route GitHub, Stripe, Slack, or custom webhooks into Hermes without opening inbound ports. clawproxy accepts the public request, stores it, and delivers it to Hermes when your node is configured with an outbound connection.

+
+
+

delivery target

Hermes stays private. Work still starts instantly.

Public webhooks become reliable inputs for agent workflows, Kanban dispatch, and automation runs.

{steps.map((step, index) => (

{String(index + 1).padStart(2, "0")}

{step}

))}
+
+
{["Trigger Kanban workers from GitHub", "Keep agent boxes private", "Debug webhook delivery"].map((title, index) => (

{String(index + 1).padStart(2, "0")}

{title}

Use clawproxy as the durable public front door for Hermes automation that should stay behind outbound-only network boundaries.

))}
+

Connect Hermes with one public route.

Create a clawproxy route, copy its generated provider URL and node token from the dashboard, then install the Hermes plugin. A clawproxy route named github-prs delivers to the Hermes webhook route with the same slug.

setup snippet

{snippet}
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index ac2925e..d452ee3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -164,6 +164,10 @@ export default function Home() {

Works with Hermes Agent, OpenClaw, and any client that can receive events from an authenticated outbound connection. {' '} + + Using Hermes? + + {' '} Using OpenClaw? diff --git a/tests/landing/copy.test.ts b/tests/landing/copy.test.ts index 2d4e956..4cefad1 100644 --- a/tests/landing/copy.test.ts +++ b/tests/landing/copy.test.ts @@ -24,11 +24,17 @@ describe('landing page copy', () => { expect(html).not.toContain('private OpenClaw nodes'); }); + test('includes a contextual entry point for Hermes users', async () => { + const html = await landingHtml(); + + expect(html).toContain('href="/hermes"'); + expect(html).toContain('Using Hermes?'); + }); + test('includes a contextual entry point for OpenClaw users', async () => { const html = await landingHtml(); expect(html).toContain('href="/openclaw"'); expect(html).toContain('Using OpenClaw?'); }); - }); diff --git a/tests/landing/hermes-page.test.ts b/tests/landing/hermes-page.test.ts new file mode 100644 index 0000000..6544fd0 --- /dev/null +++ b/tests/landing/hermes-page.test.ts @@ -0,0 +1,39 @@ +import { JSDOM } from "jsdom"; +import { describe, expect, test } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +async function hermesHtml() { + const hermesPage = await import("@/app/hermes/page"); + return renderToStaticMarkup(hermesPage.default()); +} + +describe("Hermes landing page", () => { + test("targets Hermes agent webhook relay workflows", async () => { + const html = await hermesHtml(); + expect(html).toContain("Webhook relay for Hermes agent workflows"); + expect(html).toContain("Route GitHub, Stripe, Slack, or custom webhooks into Hermes"); + expect(html).toContain("configured with an outbound connection"); + }); + + test("includes a setup snippet and relevant CTAs", async () => { + const html = await hermesHtml(); + const dom = new JSDOM(html); + const document = dom.window.document; + const snippet = document.querySelector("pre code")?.textContent ?? ""; + expect(snippet).toContain("hermes plugins install ericdahl-dev/clawproxy-hermes"); + expect(snippet).toContain("CLAWPROXY_NODE_TOKEN=cpn_your_node_token"); + expect(snippet).toContain("CLAWPROXY_HERMES_WEBHOOK_SECRET="); + expect(snippet).toContain("clawproxy.io/api/ingress//"); + expect(snippet).not.toContain("/api/ingress/hermes"); + expect(snippet).not.toContain("webhooks.relay_url"); + expect(document.querySelector("a[href=\"/dashboard\"]")?.textContent).toContain("Create a route"); + expect(document.querySelector("a[href=\"https://github.com/ericdahl-dev/clawproxy-hermes\"]")?.textContent).toContain("Hermes plugin"); + }); + + test("shows the Hermes-specific delivery path", async () => { + const html = await hermesHtml(); + expect(html).toContain("Provider webhook"); + expect(html).toContain("clawproxy route"); + expect(html).toContain("Hermes agent run"); + }); +});