diff --git a/apps/broker/src/emails/mesh-invitation.tsx b/apps/broker/src/emails/mesh-invitation.tsx index 23fddc9..ac540f2 100644 --- a/apps/broker/src/emails/mesh-invitation.tsx +++ b/apps/broker/src/emails/mesh-invitation.tsx @@ -9,7 +9,6 @@ import { Link, Preview, Section, - Tailwind, Text, } from "@react-email/components"; import * as React from "react"; @@ -17,84 +16,295 @@ import * as React from "react"; interface MeshInvitationProps { meshName: string; inviteUrl: string; + token: string; expiresAt: string; appBaseUrl: string; } +// Brand tokens — mirror of apps/web/src/assets/styles/globals.css (--cm-*). +// Inlined here because email clients don't resolve CSS vars. +const brand = { + bg: "#141413", + bgElevated: "#1f1e1d", + bgCode: "#0f0e0d", + fg: "#faf9f5", + fgSecondary: "#c2c0b6", + fgTertiary: "#87867f", + clay: "#d97757", + clayBorder: "rgba(217, 119, 87, 0.35)", + border: "rgba(217, 119, 87, 0.2)", + serif: 'Georgia, "Times New Roman", serif', + mono: '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace', + sans: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif', +} as const; + export const MeshInvitation = ({ meshName, inviteUrl, + token, expiresAt, appBaseUrl, }: MeshInvitationProps) => { const expiresLabel = new Date(expiresAt).toUTCString(); + const launchCmd = `claudemesh launch --join ${inviteUrl}`; + const oneLiner = `npm i -g claudemesh-cli && ${launchCmd}`; return ( - + + + + You've been invited to the {meshName} mesh on claudemesh - - - -
- - ◇ claudemesh - -
+ + + {/* Header — mesh glyph + wordmark */} +
+ + + + + +
+ + + + + + + + + + claudemesh + +
+
- - You're invited to join{" "} - {meshName} - + {/* Eyebrow */} + + — you're invited + - - Someone invited you to join their mesh on claudemesh — a peer - network for Claude Code sessions. Accept the invite to connect - your session with theirs. + {/* Heading */} + + Join{" "} + + {meshName} + {" "} + on claudemesh + + + {/* Body prose */} + + claudemesh is a peer mesh for Claude Code sessions — end-to-end + encrypted, keys stay on your machine. Open the link below to see + the mesh, the inviter, and the command to join. + + + {/* Primary CTA */} +
+ +
+ + {/* Terminal shortcut — for the already-set-up crowd */} + + — already have the CLI? + +
+ + {launchCmd} +
-
- -
- - - Or copy this link into your browser: + {/* First-time one-liner */} + + — first time? one command + +
+ + {oneLiner} - - - {inviteUrl} - + + Requires Node.js 20+. Display name defaults to $USER. +
-
+
- - This invite expires on{" "} - {expiresLabel}. If you - weren't expecting this email, you can safely ignore it. - -
- - - - - claudemesh.com - - - - -
+ {/* Footer meta */} + + Expires{" "} + {expiresLabel}. + If you weren't expecting this, you can ignore it. + + + + claudemesh.com + + + + ); }; @@ -102,6 +312,7 @@ export const MeshInvitation = ({ MeshInvitation.PreviewProps = { meshName: "prueba1", inviteUrl: "https://claudemesh.com/i/RUVMYXZQ", + token: "eyJ2IjoxLCJtZXNoX2lkIjoiQUtMYUZxR3FKOGZCajN0U3dvVk1PSFYxQmF3UGlYTE8iLCJtZXNoX3NsdWciOiJwcnVlYmExIn0", expiresAt: "2026-04-22T00:51:26.181Z", appBaseUrl: "https://claudemesh.com", } satisfies MeshInvitationProps; diff --git a/apps/broker/src/index.ts b/apps/broker/src/index.ts index 432d826..28cbfe7 100644 --- a/apps/broker/src/index.ts +++ b/apps/broker/src/index.ts @@ -5143,7 +5143,7 @@ async function handleCliMeshInvite(req: IncomingMessage, slug: string, res: Serv const { MeshInvitation } = await import("./emails/mesh-invitation"); const React = await import("react"); const subject = `You're invited to join "${m.name}" on claudemesh`; - const element = React.createElement(MeshInvitation, { meshName: m.name, inviteUrl: url, expiresAt: expiresAt.toISOString(), appBaseUrl: baseUrl }); + const element = React.createElement(MeshInvitation, { meshName: m.name, inviteUrl: url, token, expiresAt: expiresAt.toISOString(), appBaseUrl: baseUrl }); const html = await render(element); const text = await render(element, { plainText: true }); const res = process.env.POSTMARK_API_KEY diff --git a/apps/web/src/modules/join/install-toggle.tsx b/apps/web/src/modules/join/install-toggle.tsx index aed83a8..2f545db 100644 --- a/apps/web/src/modules/join/install-toggle.tsx +++ b/apps/web/src/modules/join/install-toggle.tsx @@ -5,8 +5,9 @@ interface Props { token: string; } -const LAUNCH_CMD = (token: string) => `claudemesh launch --name YourName --join ${token}`; -const JOIN_CMD = (token: string) => `claudemesh join ${token}`; +const LAUNCH_CMD = (token: string) => `claudemesh launch --join ${token}`; +const INSTALL_AND_LAUNCH = (token: string) => + `npm i -g claudemesh-cli && claudemesh launch --join ${token}`; const INSTALL_CMD = "npm i -g claudemesh-cli"; export const InstallToggle = ({ token }: Props) => { @@ -97,71 +98,39 @@ export const InstallToggle = ({ token }: Props) => { ); } - const launchCmd = LAUNCH_CMD(token); + const oneLiner = INSTALL_AND_LAUNCH(token); return (
-
    -
  1. -
    +
    + install + launch — one command +
    +
    + - 1 - install the CLI -
    -
    - - {INSTALL_CMD} - - -
    -

    +

  2. -
  3. -
    - 2 - join + launch -
    -
    - - {launchCmd} - - -
    -

    - Joins the mesh and launches Claude Code in one step. -

    -
  4. -
+ {copiedKey === "one" ? "Copied ✓" : "Copy"} + +
+

+ Requires Node.js 20+. Your display name defaults to $USER — override with{" "} + --name YourName. +

+