diff --git a/apps/cli/README.md b/apps/cli/README.md index fd14155..0d9f1db 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -25,7 +25,7 @@ Run the printed command, then restart Claude Code. ## Join a mesh ```sh -claudemesh join ic://join/BASE64URL... +claudemesh join https://claudemesh.com/join/ ``` The invite link is generated by whoever runs the mesh. It bundles the @@ -37,7 +37,7 @@ the result to `~/.claudemesh/config.json`. ```sh claudemesh install # print MCP registration command -claudemesh join # join a mesh via invite link +claudemesh join # join a mesh via invite URL claudemesh list # show joined meshes + identities claudemesh leave # leave a mesh claudemesh mcp # start MCP server (stdio — Claude Code only) diff --git a/apps/cli/src/commands/install.ts b/apps/cli/src/commands/install.ts index 684a7dd..3974594 100644 --- a/apps/cli/src/commands/install.ts +++ b/apps/cli/src/commands/install.ts @@ -185,7 +185,9 @@ export function runInstall(): void { console.log(""); console.log(yellow(bold("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear.")); console.log(""); - console.log(`Next: ${bold("claudemesh join ic://join/")}`); + console.log( + `Next: ${bold("claudemesh join https://claudemesh.com/join/")}`, + ); } export function runUninstall(): void { diff --git a/apps/cli/src/commands/join.ts b/apps/cli/src/commands/join.ts index 7630e8f..cfd9a50 100644 --- a/apps/cli/src/commands/join.ts +++ b/apps/cli/src/commands/join.ts @@ -19,9 +19,11 @@ import { hostname } from "node:os"; export async function runJoin(args: string[]): Promise { const link = args[0]; if (!link) { - console.error("Usage: claudemesh join "); + console.error("Usage: claudemesh join "); console.error(""); - console.error("Example: claudemesh join ic://join/eyJ2IjoxLC4uLn0"); + console.error( + "Example: claudemesh join https://claudemesh.com/join/eyJ2IjoxLC4uLn0", + ); process.exit(1); } diff --git a/apps/cli/src/commands/list.ts b/apps/cli/src/commands/list.ts index 7d80ab5..2748aed 100644 --- a/apps/cli/src/commands/list.ts +++ b/apps/cli/src/commands/list.ts @@ -9,7 +9,9 @@ export function runList(): void { if (config.meshes.length === 0) { console.log("No meshes joined yet."); console.log(""); - console.log("Join one with: claudemesh join "); + console.log( + "Join one with: claudemesh join https://claudemesh.com/join/", + ); console.log(`Config file: ${getConfigPath()}`); return; } diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 16679b9..5cd8a81 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -23,7 +23,7 @@ Usage: Commands: install Register claudemesh as a Claude Code MCP server uninstall Remove claudemesh MCP server registration - join Join a mesh via invite link (ic://join/...) + join Join a mesh via https://claudemesh.com/join/... URL list Show all joined meshes leave Leave a joined mesh seed-test-mesh Dev-only: inject a mesh into config (skips invite flow) diff --git a/apps/cli/src/invite/parse.ts b/apps/cli/src/invite/parse.ts index cf91eb3..583ad15 100644 --- a/apps/cli/src/invite/parse.ts +++ b/apps/cli/src/invite/parse.ts @@ -42,14 +42,41 @@ export function canonicalInvite(p: { return `${p.v}|${p.mesh_id}|${p.mesh_slug}|${p.broker_url}|${p.expires_at}|${p.mesh_root_key}|${p.role}|${p.owner_pubkey}`; } -export async function parseInviteLink(link: string): Promise { - if (!link.startsWith("ic://join/")) { - throw new Error( - `invalid invite link: expected prefix "ic://join/", got "${link.slice(0, 20)}…"`, - ); +/** + * Extract the raw base64url token from any accepted invite input. + * + * Accepts three formats: + * - `ic://join/` (dev-era scheme, still supported) + * - `https://claudemesh.com/join/` (clickable landing page) + * - `https://claudemesh.com//join/` (i18n prefix) + * - `` (raw base64url, last resort) + */ +export function extractInviteToken(input: string): string { + const trimmed = input.trim(); + if (trimmed.startsWith("ic://join/")) { + const token = trimmed.slice("ic://join/".length).replace(/\/$/, ""); + if (!token) throw new Error("invite link has no payload"); + return token; } - const encoded = link.slice("ic://join/".length); - if (!encoded) throw new Error("invite link has no payload"); + const httpsMatch = trimmed.match( + /^https?:\/\/[^/]+(?:\/[a-z]{2})?\/join\/([A-Za-z0-9_-]+)\/?$/, + ); + if (httpsMatch) return httpsMatch[1]!; + // Last resort: treat as raw base64url token. + if (/^[A-Za-z0-9_-]+$/.test(trimmed) && trimmed.length > 20) { + return trimmed; + } + throw new Error( + `invalid invite format. Expected one of:\n` + + ` https://claudemesh.com/join/\n` + + ` ic://join/\n` + + ` \n` + + `Got: "${input.slice(0, 40)}${input.length > 40 ? "…" : ""}"`, + ); +} + +export async function parseInviteLink(link: string): Promise { + const encoded = extractInviteToken(link); let json: string; try { diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index b92e989..40f164a 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -103,7 +103,7 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w const { name, arguments: args } = req.params; if (config.meshes.length === 0) { return text( - "No meshes joined. Run `claudemesh join ` first.", + "No meshes joined. Run `claudemesh join https://claudemesh.com/join/` first.", true, ); } diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index fb876ae..30d447c 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -67,12 +67,15 @@ You have two paths. Pick one. ### Path A — join a teammate's mesh *(fastest)* -Paste the invite link they sent you (starts with `ic://join/…`): +Paste the invite URL they sent you: ```sh -claudemesh join ic://join/eyJtZXNo... +claudemesh join https://claudemesh.com/join/eyJtZXNo... ``` +(The CLI also accepts `ic://join/` and raw tokens if you have +those instead.) + The CLI verifies the signature, generates a fresh keypair for you, and enrolls you with the broker: @@ -87,10 +90,10 @@ and enrolls you with the broker: 1. Open **[claudemesh.com](https://claudemesh.com)** and sign up 2. Click **Create mesh**, give it a slug (e.g. `my-team`) -3. Copy the invite link it generates +3. Copy the invite URL it generates 4. Back in your terminal: ```sh - claudemesh join ic://join/ + claudemesh join https://claudemesh.com/join/ ``` ---