feat(cli): accept https://claudemesh.com/join/<token> invite URL format
Pairs with claudemesh-2's new /join/[token] landing page. Users can now paste a clickable HTTPS URL instead of the dev-only ic:// scheme. apps/cli/src/invite/parse.ts — new extractInviteToken() handles four input formats before handing the raw base64url token to the existing parseInviteLink pipeline: - https://claudemesh.com/join/<token> (primary, clickable) - https://claudemesh.com/<locale>/join/<token> (i18n prefix) - ic://join/<token> (still supported, dev) - <raw-token> (last resort: bare base64url) User-facing strings updated to the HTTPS form: - cli help: "join <url>" - install success message - list (no-meshes) hint - MCP server "no meshes" error - README.md primary example - docs/QUICKSTART.md Path A + Path B Verified extractInviteToken() on all 4 formats — each returns the same base64url token → same broker /join lookup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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/<token>
|
||||
```
|
||||
|
||||
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 <link> # join a mesh via invite link
|
||||
claudemesh join <url> # join a mesh via invite URL
|
||||
claudemesh list # show joined meshes + identities
|
||||
claudemesh leave <slug> # leave a mesh
|
||||
claudemesh mcp # start MCP server (stdio — Claude Code only)
|
||||
|
||||
@@ -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/<your-invite-link>")}`);
|
||||
console.log(
|
||||
`Next: ${bold("claudemesh join https://claudemesh.com/join/<token>")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function runUninstall(): void {
|
||||
|
||||
@@ -19,9 +19,11 @@ import { hostname } from "node:os";
|
||||
export async function runJoin(args: string[]): Promise<void> {
|
||||
const link = args[0];
|
||||
if (!link) {
|
||||
console.error("Usage: claudemesh join <invite-link>");
|
||||
console.error("Usage: claudemesh join <invite-url-or-token>");
|
||||
console.error("");
|
||||
console.error("Example: claudemesh join ic://join/eyJ2IjoxLC4uLn0");
|
||||
console.error(
|
||||
"Example: claudemesh join https://claudemesh.com/join/eyJ2IjoxLC4uLn0",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <invite-link>");
|
||||
console.log(
|
||||
"Join one with: claudemesh join https://claudemesh.com/join/<token>",
|
||||
);
|
||||
console.log(`Config file: ${getConfigPath()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Usage:
|
||||
Commands:
|
||||
install Register claudemesh as a Claude Code MCP server
|
||||
uninstall Remove claudemesh MCP server registration
|
||||
join <link> Join a mesh via invite link (ic://join/...)
|
||||
join <url> Join a mesh via https://claudemesh.com/join/... URL
|
||||
list Show all joined meshes
|
||||
leave <slug> Leave a joined mesh
|
||||
seed-test-mesh Dev-only: inject a mesh into config (skips invite flow)
|
||||
|
||||
@@ -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<ParsedInvite> {
|
||||
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/<token>` (dev-era scheme, still supported)
|
||||
* - `https://claudemesh.com/join/<token>` (clickable landing page)
|
||||
* - `https://claudemesh.com/<locale>/join/<token>` (i18n prefix)
|
||||
* - `<token>` (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/<token>\n` +
|
||||
` ic://join/<token>\n` +
|
||||
` <raw-token>\n` +
|
||||
`Got: "${input.slice(0, 40)}${input.length > 40 ? "…" : ""}"`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function parseInviteLink(link: string): Promise<ParsedInvite> {
|
||||
const encoded = extractInviteToken(link);
|
||||
|
||||
let json: string;
|
||||
try {
|
||||
|
||||
@@ -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 <invite-link>` first.",
|
||||
"No meshes joined. Run `claudemesh join https://claudemesh.com/join/<token>` first.",
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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/<token>` 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/<your-link>
|
||||
claudemesh join https://claudemesh.com/join/<token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user