Compare commits
7 Commits
cli-v0.1.2
...
a1c6c6dc6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c6c6dc6a | ||
|
|
00b5ba8190 | ||
|
|
ccff802163 | ||
|
|
231618c595 | ||
|
|
f698aaeac7 | ||
|
|
8810aa1e9e | ||
|
|
fa234fae25 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
212
apps/cli/src/commands/doctor.ts
Normal file
212
apps/cli/src/commands/doctor.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* `claudemesh doctor` — diagnostic checks.
|
||||||
|
*
|
||||||
|
* Walks through the install + runtime preconditions and prints each
|
||||||
|
* as pass/fail with a fix hint on failure. Exit 0 if everything
|
||||||
|
* passes, 1 otherwise.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { homedir, platform } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { loadConfig, getConfigPath } from "../state/config";
|
||||||
|
import { VERSION } from "../version";
|
||||||
|
|
||||||
|
interface Check {
|
||||||
|
name: string;
|
||||||
|
pass: boolean;
|
||||||
|
detail?: string;
|
||||||
|
fix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNode(): Check {
|
||||||
|
const major = Number(process.versions.node.split(".")[0]);
|
||||||
|
return {
|
||||||
|
name: "Node.js >= 20",
|
||||||
|
pass: major >= 20,
|
||||||
|
detail: `v${process.versions.node}`,
|
||||||
|
fix: "Install Node 20 or newer (https://nodejs.org)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkClaudeOnPath(): Check {
|
||||||
|
const res =
|
||||||
|
platform() === "win32"
|
||||||
|
? spawnSync("where", ["claude"])
|
||||||
|
: spawnSync("sh", ["-c", "command -v claude"]);
|
||||||
|
const onPath = res.status === 0;
|
||||||
|
const location = onPath ? res.stdout.toString().trim().split("\n")[0] : undefined;
|
||||||
|
return {
|
||||||
|
name: "claude binary on PATH",
|
||||||
|
pass: onPath,
|
||||||
|
detail: location,
|
||||||
|
fix: "Install Claude Code (https://claude.com/claude-code)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMcpRegistered(): Check {
|
||||||
|
const claudeConfig = join(homedir(), ".claude.json");
|
||||||
|
if (!existsSync(claudeConfig)) {
|
||||||
|
return {
|
||||||
|
name: "claudemesh MCP registered in ~/.claude.json",
|
||||||
|
pass: false,
|
||||||
|
fix: "Run `claudemesh install`",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(readFileSync(claudeConfig, "utf-8")) as {
|
||||||
|
mcpServers?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
const registered = Boolean(cfg.mcpServers?.["claudemesh"]);
|
||||||
|
return {
|
||||||
|
name: "claudemesh MCP registered in ~/.claude.json",
|
||||||
|
pass: registered,
|
||||||
|
fix: registered ? undefined : "Run `claudemesh install`",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
name: "claudemesh MCP registered in ~/.claude.json",
|
||||||
|
pass: false,
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
fix: "Check ~/.claude.json for JSON parse errors",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHooksRegistered(): Check {
|
||||||
|
const settings = join(homedir(), ".claude", "settings.json");
|
||||||
|
if (!existsSync(settings)) {
|
||||||
|
return {
|
||||||
|
name: "Status hooks registered in ~/.claude/settings.json",
|
||||||
|
pass: false,
|
||||||
|
fix: "Run `claudemesh install` (remove --no-hooks)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(settings, "utf-8");
|
||||||
|
const has = raw.includes("claudemesh hook ");
|
||||||
|
return {
|
||||||
|
name: "Status hooks registered in ~/.claude/settings.json",
|
||||||
|
pass: has,
|
||||||
|
fix: has ? undefined : "Run `claudemesh install` (remove --no-hooks)",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
name: "Status hooks registered in ~/.claude/settings.json",
|
||||||
|
pass: false,
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConfigFile(): Check {
|
||||||
|
const path = getConfigPath();
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return {
|
||||||
|
name: "~/.claudemesh/config.json exists and parses",
|
||||||
|
pass: true,
|
||||||
|
detail: "not created yet (fine — no meshes joined)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loadConfig();
|
||||||
|
const st = statSync(path);
|
||||||
|
const mode = (st.mode & 0o777).toString(8);
|
||||||
|
const secure = platform() === "win32" || mode === "600";
|
||||||
|
return {
|
||||||
|
name: "~/.claudemesh/config.json parses + chmod 0600",
|
||||||
|
pass: secure,
|
||||||
|
detail: platform() === "win32" ? "chmod skipped on Windows" : `0${mode}`,
|
||||||
|
fix: secure ? undefined : `chmod 600 ${path}`,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
name: "~/.claudemesh/config.json exists and parses",
|
||||||
|
pass: false,
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
fix: "Inspect or delete ~/.claudemesh/config.json and re-join",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkKeypairs(): Check {
|
||||||
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
if (cfg.meshes.length === 0) {
|
||||||
|
return {
|
||||||
|
name: "Mesh keypairs valid",
|
||||||
|
pass: true,
|
||||||
|
detail: "no meshes joined",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (const m of cfg.meshes) {
|
||||||
|
if (m.pubkey.length !== 64 || !/^[0-9a-f]+$/.test(m.pubkey)) {
|
||||||
|
return {
|
||||||
|
name: "Mesh keypairs valid",
|
||||||
|
pass: false,
|
||||||
|
detail: `${m.slug}: pubkey malformed`,
|
||||||
|
fix: `Leave + re-join the mesh: claudemesh leave ${m.slug}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (m.secretKey.length !== 128 || !/^[0-9a-f]+$/.test(m.secretKey)) {
|
||||||
|
return {
|
||||||
|
name: "Mesh keypairs valid",
|
||||||
|
pass: false,
|
||||||
|
detail: `${m.slug}: secret key malformed`,
|
||||||
|
fix: `Leave + re-join the mesh: claudemesh leave ${m.slug}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: "Mesh keypairs valid",
|
||||||
|
pass: true,
|
||||||
|
detail: `${cfg.meshes.length} mesh(es)`,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
name: "Mesh keypairs valid",
|
||||||
|
pass: false,
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runDoctor(): Promise<void> {
|
||||||
|
const useColor =
|
||||||
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
|
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||||
|
const red = (s: string): string => (useColor ? `\x1b[31m${s}\x1b[39m` : s);
|
||||||
|
|
||||||
|
console.log(`claudemesh doctor (v${VERSION})`);
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
const checks: Check[] = [
|
||||||
|
checkNode(),
|
||||||
|
checkClaudeOnPath(),
|
||||||
|
checkMcpRegistered(),
|
||||||
|
checkHooksRegistered(),
|
||||||
|
checkConfigFile(),
|
||||||
|
checkKeypairs(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const c of checks) {
|
||||||
|
const mark = c.pass ? green("✓") : red("✗");
|
||||||
|
const detail = c.detail ? dim(` (${c.detail})`) : "";
|
||||||
|
console.log(`${mark} ${c.name}${detail}`);
|
||||||
|
if (!c.pass && c.fix) {
|
||||||
|
console.log(dim(` → ${c.fix}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const failing = checks.filter((c) => !c.pass);
|
||||||
|
console.log("");
|
||||||
|
if (failing.length === 0) {
|
||||||
|
console.log(green("All checks passed."));
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(red(`${failing.length} check(s) failed.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
apps/cli/src/commands/status.ts
Normal file
103
apps/cli/src/commands/status.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* `claudemesh status` — one-shot health report.
|
||||||
|
*
|
||||||
|
* Reports CLI version, config path + permissions, each joined mesh
|
||||||
|
* with broker reachability (WS handshake probe). Exit 0 if every
|
||||||
|
* mesh's broker is reachable, 1 otherwise.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { statSync, existsSync } from "node:fs";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import { loadConfig, getConfigPath } from "../state/config";
|
||||||
|
import { VERSION } from "../version";
|
||||||
|
|
||||||
|
interface MeshStatus {
|
||||||
|
slug: string;
|
||||||
|
brokerUrl: string;
|
||||||
|
pubkey: string;
|
||||||
|
reachable: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeBroker(url: string, timeoutMs = 4000): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
try { ws.terminate(); } catch { /* noop */ }
|
||||||
|
resolve({ ok: false, error: "timeout" });
|
||||||
|
}, timeoutMs);
|
||||||
|
ws.on("open", () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
try { ws.close(); } catch { /* noop */ }
|
||||||
|
resolve({ ok: true });
|
||||||
|
});
|
||||||
|
ws.on("error", (err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ ok: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runStatus(): Promise<void> {
|
||||||
|
const useColor =
|
||||||
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
|
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||||
|
const red = (s: string): string => (useColor ? `\x1b[31m${s}\x1b[39m` : s);
|
||||||
|
|
||||||
|
console.log(`claudemesh status (v${VERSION})`);
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
let configPerms = "missing";
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
const st = statSync(configPath);
|
||||||
|
const mode = (st.mode & 0o777).toString(8).padStart(4, "0");
|
||||||
|
configPerms = mode === "0600" ? `${mode} ✓` : `${mode} ⚠ (expected 0600)`;
|
||||||
|
}
|
||||||
|
console.log(`Config: ${configPath} (${configPerms})`);
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
if (config.meshes.length === 0) {
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("No meshes joined. Run `claudemesh join <invite-url>` to get started."));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(`Meshes (${config.meshes.length}):`);
|
||||||
|
|
||||||
|
const results: MeshStatus[] = [];
|
||||||
|
for (const m of config.meshes) {
|
||||||
|
process.stdout.write(` ${m.slug.padEnd(20)} probing ${m.brokerUrl}… `);
|
||||||
|
const probe = await probeBroker(m.brokerUrl);
|
||||||
|
results.push({
|
||||||
|
slug: m.slug,
|
||||||
|
brokerUrl: m.brokerUrl,
|
||||||
|
pubkey: m.pubkey,
|
||||||
|
reachable: probe.ok,
|
||||||
|
error: probe.error,
|
||||||
|
});
|
||||||
|
if (probe.ok) {
|
||||||
|
console.log(green("reachable"));
|
||||||
|
} else {
|
||||||
|
console.log(red(`unreachable (${probe.error})`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
for (const r of results) {
|
||||||
|
console.log(dim(` ${r.slug}: pubkey ${r.pubkey.slice(0, 16)}…`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allOk = results.every((r) => r.reachable);
|
||||||
|
console.log("");
|
||||||
|
if (allOk) {
|
||||||
|
console.log(green("All meshes reachable."));
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
const broken = results.filter((r) => !r.reachable).length;
|
||||||
|
console.log(red(`${broken} of ${results.length} mesh(es) unreachable.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
apps/cli/src/commands/welcome.ts
Normal file
111
apps/cli/src/commands/welcome.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Stateful welcome screen — shown when the user runs `claudemesh`
|
||||||
|
* with no arguments. Detects install state + joined meshes + prints
|
||||||
|
* the next action they should take.
|
||||||
|
*
|
||||||
|
* States, in priority order:
|
||||||
|
* 1. MCP not registered in ~/.claude.json → run install
|
||||||
|
* 2. Config dir exists but no meshes joined → run join
|
||||||
|
* 3. Meshes joined, all reachable → run launch
|
||||||
|
* 4. Meshes joined, broker unreachable → run status / doctor
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { loadConfig } from "../state/config";
|
||||||
|
import { VERSION } from "../version";
|
||||||
|
|
||||||
|
type State = "no-install" | "no-meshes" | "ready" | "broken-config";
|
||||||
|
|
||||||
|
function detectState(): State {
|
||||||
|
// 1. MCP registered?
|
||||||
|
const claudeConfig = join(homedir(), ".claude.json");
|
||||||
|
let mcpRegistered = false;
|
||||||
|
if (existsSync(claudeConfig)) {
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(readFileSync(claudeConfig, "utf-8")) as {
|
||||||
|
mcpServers?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
mcpRegistered = Boolean(cfg.mcpServers?.["claudemesh"]);
|
||||||
|
} catch {
|
||||||
|
/* treat parse errors as not-registered */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mcpRegistered) return "no-install";
|
||||||
|
|
||||||
|
// 2. Config parseable + has meshes?
|
||||||
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
return cfg.meshes.length === 0 ? "no-meshes" : "ready";
|
||||||
|
} catch {
|
||||||
|
return "broken-config";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWelcome(): void {
|
||||||
|
const useColor =
|
||||||
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
|
const bold = (s: string): string => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||||
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
|
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||||
|
const yellow = (s: string): string => (useColor ? `\x1b[33m${s}\x1b[39m` : s);
|
||||||
|
|
||||||
|
console.log(bold(`claudemesh v${VERSION}`) + dim(" — peer mesh for Claude Code"));
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
const state = detectState();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "no-install":
|
||||||
|
console.log("Welcome. Let's get you set up.");
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("Step 1:") + " register the MCP server + status hooks");
|
||||||
|
console.log(` ${green("$")} claudemesh install`);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Step 2 (after restart): claudemesh join <invite-url>"));
|
||||||
|
console.log(dim("Step 3: claudemesh launch"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "no-meshes":
|
||||||
|
console.log(green("✓") + " MCP registered. Now join a mesh.");
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("Step 2:") + " join a mesh");
|
||||||
|
console.log(` ${green("$")} claudemesh join https://claudemesh.com/join/<token>`);
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
dim(" Don't have an invite? Create one at ") +
|
||||||
|
bold("https://claudemesh.com") +
|
||||||
|
dim(" or ask a mesh owner."),
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Step 3 (after joining): claudemesh launch"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ready": {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const meshNames = cfg.meshes.map((m) => m.slug).join(", ");
|
||||||
|
console.log(green("✓") + " MCP registered.");
|
||||||
|
console.log(green("✓") + ` ${cfg.meshes.length} mesh(es) joined: ${meshNames}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("You're ready.") + " Launch Claude Code with real-time peer messages:");
|
||||||
|
console.log(` ${green("$")} claudemesh launch`);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim(" (Plain `claude` works too — messages pull-only via check_messages.)"));
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Health check: claudemesh status"));
|
||||||
|
console.log(dim("Diagnostics: claudemesh doctor"));
|
||||||
|
console.log(dim("All commands: claudemesh --help"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "broken-config":
|
||||||
|
console.log(yellow("⚠") + " Your ~/.claudemesh/config.json is unreadable.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Run diagnostics to see what's wrong:");
|
||||||
|
console.log(` ${green("$")} claudemesh doctor`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -16,8 +16,12 @@ import { runLeave } from "./commands/leave";
|
|||||||
import { runSeedTestMesh } from "./commands/seed-test-mesh";
|
import { runSeedTestMesh } from "./commands/seed-test-mesh";
|
||||||
import { runHook } from "./commands/hook";
|
import { runHook } from "./commands/hook";
|
||||||
import { runLaunch } from "./commands/launch";
|
import { runLaunch } from "./commands/launch";
|
||||||
|
import { runStatus } from "./commands/status";
|
||||||
|
import { runDoctor } from "./commands/doctor";
|
||||||
|
import { runWelcome } from "./commands/welcome";
|
||||||
|
import { VERSION } from "./version";
|
||||||
|
|
||||||
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
const HELP = `claudemesh v${VERSION} — peer mesh for Claude Code sessions
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
claudemesh <command> [args]
|
claudemesh <command> [args]
|
||||||
@@ -32,9 +36,12 @@ Commands:
|
|||||||
join <url> Join a mesh via https://claudemesh.com/join/... URL
|
join <url> Join a mesh via https://claudemesh.com/join/... URL
|
||||||
list Show all joined meshes
|
list Show all joined meshes
|
||||||
leave <slug> Leave a joined mesh
|
leave <slug> Leave a joined mesh
|
||||||
|
status Health report: broker reachability per joined mesh
|
||||||
|
doctor Diagnostic checks (install, config, keypairs, PATH)
|
||||||
seed-test-mesh Dev-only: inject a mesh into config (skips invite flow)
|
seed-test-mesh Dev-only: inject a mesh into config (skips invite flow)
|
||||||
mcp Start MCP server (stdio) — invoked by Claude Code
|
mcp Start MCP server (stdio) — invoked by Claude Code
|
||||||
--help, -h Show this help
|
--help, -h Show this help
|
||||||
|
--version, -v Show the CLI version
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
CLAUDEMESH_BROKER_URL Override broker URL (default: wss://ic.claudemesh.com/ws)
|
CLAUDEMESH_BROKER_URL Override broker URL (default: wss://ic.claudemesh.com/ws)
|
||||||
@@ -71,15 +78,28 @@ async function main(): Promise<void> {
|
|||||||
case "leave":
|
case "leave":
|
||||||
runLeave(args);
|
runLeave(args);
|
||||||
return;
|
return;
|
||||||
|
case "status":
|
||||||
|
await runStatus();
|
||||||
|
return;
|
||||||
|
case "doctor":
|
||||||
|
await runDoctor();
|
||||||
|
return;
|
||||||
case "seed-test-mesh":
|
case "seed-test-mesh":
|
||||||
runSeedTestMesh(args);
|
runSeedTestMesh(args);
|
||||||
return;
|
return;
|
||||||
|
case "--version":
|
||||||
|
case "-v":
|
||||||
|
case "version":
|
||||||
|
console.log(VERSION);
|
||||||
|
return;
|
||||||
case "--help":
|
case "--help":
|
||||||
case "-h":
|
case "-h":
|
||||||
case "help":
|
case "help":
|
||||||
case undefined:
|
|
||||||
console.log(HELP);
|
console.log(HELP);
|
||||||
return;
|
return;
|
||||||
|
case undefined:
|
||||||
|
runWelcome();
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown command: ${cmd}`);
|
console.error(`Unknown command: ${cmd}`);
|
||||||
console.error("Run `claudemesh --help` for usage.");
|
console.error("Run `claudemesh --help` for usage.");
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function startMcpServer(): Promise<void> {
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.1.2" },
|
{ name: "claudemesh", version: "0.1.4" },
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
experimental: { "claude/channel": {} },
|
experimental: { "claude/channel": {} },
|
||||||
|
|||||||
8
apps/cli/src/version.ts
Normal file
8
apps/cli/src/version.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Bundled version string. Bun inlines the package.json JSON at build
|
||||||
|
* time, so the shipped binary carries the exact version that was
|
||||||
|
* published.
|
||||||
|
*/
|
||||||
|
import pkg from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
|
export const VERSION: string = pkg.version;
|
||||||
100
apps/web/src/app/install/route.ts
Normal file
100
apps/web/src/app/install/route.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* GET /install — serves a shell installer for claudemesh-cli.
|
||||||
|
*
|
||||||
|
* Intended to be piped into bash:
|
||||||
|
* curl -fsSL https://claudemesh.com/install | bash
|
||||||
|
*
|
||||||
|
* The script is kept short + auditable. It does not try to install
|
||||||
|
* Node for the user — it checks for a compatible Node + npm and
|
||||||
|
* directs them to install Node themselves if missing. Running `bash`
|
||||||
|
* against a domain you do not fully trust is always a risk; publishing
|
||||||
|
* the script this way (rather than obfuscating it behind a binary
|
||||||
|
* blob) lets security-conscious users inspect before executing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SCRIPT = `#!/usr/bin/env bash
|
||||||
|
# claudemesh-cli installer
|
||||||
|
# Source: https://claudemesh.com/install
|
||||||
|
# Audit: curl -fsSL https://claudemesh.com/install | less
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED=$'\\033[31m'; GREEN=$'\\033[32m'; DIM=$'\\033[2m'; BOLD=$'\\033[1m'; RESET=$'\\033[0m'
|
||||||
|
|
||||||
|
say() { printf "%s\\n" "$*"; }
|
||||||
|
ok() { printf "%s✓%s %s\\n" "\${GREEN}" "\${RESET}" "$*"; }
|
||||||
|
err() { printf "%s✗%s %s\\n" "\${RED}" "\${RESET}" "$*" >&2; }
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "\${BOLD}claudemesh-cli installer\${RESET}"
|
||||||
|
say "$(printf '%.0s─' {1..40})"
|
||||||
|
|
||||||
|
# --- preflight ------------------------------------------------------
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
err "Node.js is not installed."
|
||||||
|
say " Install Node.js 20 or newer: \${BOLD}https://nodejs.org\${RESET}"
|
||||||
|
say " Or via nvm: \${DIM}curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
||||||
|
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||||
|
err "Node.js $(node -v) is too old — claudemesh-cli needs >= 20."
|
||||||
|
say " Upgrade: \${BOLD}https://nodejs.org\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Node.js $(node -v)"
|
||||||
|
|
||||||
|
if ! command -v npm >/dev/null 2>&1; then
|
||||||
|
err "npm is not installed (usually ships with Node)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "npm $(npm -v)"
|
||||||
|
|
||||||
|
# --- install --------------------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "Installing \${BOLD}claudemesh-cli\${RESET} from npm…"
|
||||||
|
if ! npm install -g claudemesh-cli; then
|
||||||
|
err "npm install failed."
|
||||||
|
say " If this is a permissions error on macOS/Linux, try:"
|
||||||
|
say " \${DIM}sudo npm install -g claudemesh-cli\${RESET}"
|
||||||
|
say " or configure npm to use a user-owned prefix:"
|
||||||
|
say " \${DIM}https://docs.npmjs.com/resolving-eacces-permissions-errors\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "claudemesh-cli installed ($(claudemesh --version))"
|
||||||
|
|
||||||
|
# --- register MCP + hooks ------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "Registering Claude Code MCP server + status hooks…"
|
||||||
|
if ! claudemesh install; then
|
||||||
|
err "claudemesh install failed — run it manually to see the error."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- done -----------------------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "\${GREEN}\${BOLD}Done.\${RESET}"
|
||||||
|
say ""
|
||||||
|
say "Next steps:"
|
||||||
|
say " 1. Restart Claude Code so the MCP tools appear."
|
||||||
|
say " 2. Join a mesh: \${BOLD}claudemesh join <invite-url>\${RESET}"
|
||||||
|
say " 3. Launch with push: \${BOLD}claudemesh launch\${RESET}"
|
||||||
|
say ""
|
||||||
|
say "Need an invite? Visit \${BOLD}https://claudemesh.com\${RESET}"
|
||||||
|
say ""
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function GET(): Response {
|
||||||
|
return new Response(SCRIPT, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/x-shellscript; charset=utf-8",
|
||||||
|
"Cache-Control": "public, max-age=300, s-maxage=600",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ export const CallToAction = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="#docs"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-6 py-3.5 text-[15px] font-medium text-[var(--cm-fg)] transition-colors duration-300 hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg-elevated)]"
|
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-6 py-3.5 text-[15px] font-medium text-[var(--cm-fg)] transition-colors duration-300 hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg-elevated)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { Reveal } from "./_reveal";
|
|||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{
|
{
|
||||||
q: "Is claudemesh free?",
|
q: "Is claudemesh free?",
|
||||||
a: "Yes — the broker, CLI, dashboard, and SDK are MIT-licensed and free forever. Solo developers and small teams can self-host at no cost. Paid tiers add hosted brokers, SSO, audit retention, and support.",
|
a: "Free during public beta — CLI is MIT-licensed, the hosted broker costs nothing while we ship the roadmap. Paid tiers launch when the dashboard ships. Beta users keep the free plan for life.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "How do I get started?",
|
q: "How do I get started?",
|
||||||
a: "Install the broker with one curl command. Add one env var to your Claude Code config. Your session joins the mesh. `npx claudemesh init` does both in 60 seconds.",
|
a: "One command: `curl -fsSL claudemesh.com/install | bash`. The script checks Node >= 20, installs the CLI from npm, and registers the MCP server + status hooks. Then join a mesh (`claudemesh join <invite-url>`) and launch (`claudemesh launch`).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "Does claudemesh send my code or prompts to the cloud?",
|
q: "Does claudemesh send my code or prompts to the cloud?",
|
||||||
@@ -29,7 +29,7 @@ const ITEMS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "Which Claude Code versions work with claudemesh?",
|
q: "Which Claude Code versions work with claudemesh?",
|
||||||
a: "Claude Code 2.0 and above. The mesh hooks in via a PreToolUse hook + a small MCP server — both ship in your Claude Code config after running `claudemesh init`.",
|
a: "Claude Code 2.0 and above. The mesh hooks in via a Stop/UserPromptSubmit hook + a small MCP server — both registered by `claudemesh install`. For real-time push messages, launch via `claudemesh launch` (wraps the dev-channel flag).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "How is this different from MCP?",
|
q: "How is this different from MCP?",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const Features = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
<button
|
<button
|
||||||
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
||||||
aria-label="Copy"
|
aria-label="Copy"
|
||||||
@@ -61,7 +61,7 @@ export const Features = () => {
|
|||||||
>
|
>
|
||||||
Free forever for solo developers · Or read the{" "}
|
Free forever for solo developers · Or read the{" "}
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
||||||
>
|
>
|
||||||
documentation
|
documentation
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import Link from "next/link";
|
|||||||
import { Reveal, SectionIcon } from "./_reveal";
|
import { Reveal, SectionIcon } from "./_reveal";
|
||||||
|
|
||||||
const LOGOS = [
|
const LOGOS = [
|
||||||
"Vercel",
|
"Claude Code",
|
||||||
"Linear",
|
"MCP",
|
||||||
"Stripe",
|
"libsodium",
|
||||||
"Supabase",
|
"Bun",
|
||||||
"Shopify",
|
"TypeScript",
|
||||||
"Figma",
|
"MIT",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Hero = () => {
|
export const Hero = () => {
|
||||||
@@ -55,11 +55,12 @@ export const Hero = () => {
|
|||||||
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
Peer mesh for Claude — reachable from anywhere you are. Connect
|
Peer mesh for Claude Code. Connect your sessions across repos and
|
||||||
every Claude Code session on your team, then bridge the mesh to
|
machines. Messages are end-to-end encrypted, delivered mid-turn
|
||||||
WhatsApp, Slack, your phone. Terminal is one client, not THE client.
|
as {"`<channel>`"} reminders. Your Claudes talk to each other; the
|
||||||
|
broker never sees plaintext.
|
||||||
<span className="block pt-2 text-[var(--cm-clay)]">
|
<span className="block pt-2 text-[var(--cm-clay)]">
|
||||||
Free and open-source. Forever.
|
Open-source CLI. Free during public beta.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -81,7 +82,7 @@ export const Hero = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -93,7 +94,7 @@ export const Hero = () => {
|
|||||||
>
|
>
|
||||||
Or{" "}
|
Or{" "}
|
||||||
<Link
|
<Link
|
||||||
href="#docs"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
||||||
>
|
>
|
||||||
read the documentation
|
read the documentation
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const LaptopToLaptop = () => {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={3} className="mt-10 flex justify-center">
|
<Reveal delay={3} className="mt-10 flex justify-center">
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="/auth/register"
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-3 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-3 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const CARDS = [
|
|||||||
accent: "clay",
|
accent: "clay",
|
||||||
title: "Start in your terminal",
|
title: "Start in your terminal",
|
||||||
body: "Drop the broker next to Claude Code. One env var. Your session joins the mesh.",
|
body: "Drop the broker next to Claude Code. One env var. Your session joins the mesh.",
|
||||||
cta: { label: "Install", href: "#" },
|
cta: { label: "Install", href: "https://github.com/alezmad/claudemesh-cli#install" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] bg-[#D97757] p-6 font-mono text-[11px] leading-[1.6] text-[#141413]"
|
className="rounded-[8px] bg-[#D97757] p-6 font-mono text-[11px] leading-[1.6] text-[#141413]"
|
||||||
@@ -26,8 +26,8 @@ const CARDS = [
|
|||||||
accent: "oat",
|
accent: "oat",
|
||||||
title: "Bridge to your editor",
|
title: "Bridge to your editor",
|
||||||
body: "VS Code, Cursor, JetBrains — the mesh exposes an MCP server your editor's agent can call.",
|
body: "VS Code, Cursor, JetBrains — the mesh exposes an MCP server your editor's agent can call.",
|
||||||
cta: { label: "VS Code", href: "#" },
|
cta: { label: "VS Code", href: "https://github.com/alezmad/claudemesh-cli#readme" },
|
||||||
cta2: { label: "JetBrains", href: "#" },
|
cta2: { label: "JetBrains", href: "https://github.com/alezmad/claudemesh-cli#readme" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
||||||
@@ -52,7 +52,7 @@ const CARDS = [
|
|||||||
accent: "cactus",
|
accent: "cactus",
|
||||||
title: "Reach across machines",
|
title: "Reach across machines",
|
||||||
body: "Tailscale, WireGuard, or plain WS over your LAN. The broker is one binary, anywhere.",
|
body: "Tailscale, WireGuard, or plain WS over your LAN. The broker is one binary, anywhere.",
|
||||||
cta: { label: "Open the dashboard", href: "#" },
|
cta: { label: "Open the dashboard", href: "/dashboard" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
||||||
|
|||||||
@@ -1,64 +1,25 @@
|
|||||||
"use client";
|
|
||||||
import { useState } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Reveal, SectionIcon } from "./_reveal";
|
import { Reveal, SectionIcon } from "./_reveal";
|
||||||
|
|
||||||
const TIERS = {
|
const SHIPPING = [
|
||||||
individual: [
|
"CLI + MCP server (Claude Code integration)",
|
||||||
{
|
"Hosted broker on claudemesh.com",
|
||||||
name: "Solo",
|
"End-to-end encrypted direct messages (crypto_box)",
|
||||||
desc: "Run the broker on your laptop. Pair your Claude Code sessions across repos.",
|
"Priority routing (now / next / low)",
|
||||||
price: "Free",
|
"Mesh invites + membership",
|
||||||
cta: "Start free",
|
"Windows, macOS, Linux support",
|
||||||
href: "/auth/register",
|
];
|
||||||
},
|
|
||||||
{
|
const ROADMAP = [
|
||||||
name: "Pro",
|
"Mesh dashboard (browser UI)",
|
||||||
desc: "Mesh dashboard, peer registry, message history, priority routing.",
|
"Message history + retention controls",
|
||||||
price: "$12",
|
"Audit log",
|
||||||
note: "per month",
|
"Slack / WhatsApp / Telegram gateways",
|
||||||
cta: "Start free trial",
|
"Self-host broker + SSO",
|
||||||
href: "/auth/register",
|
"Cross-broker federation",
|
||||||
},
|
];
|
||||||
{
|
|
||||||
name: "Plus",
|
|
||||||
desc: "Cross-machine mesh via Tailscale / WireGuard, MCP bridge, audit log.",
|
|
||||||
price: "$24",
|
|
||||||
note: "per month",
|
|
||||||
cta: "Start free trial",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
team: [
|
|
||||||
{
|
|
||||||
name: "Team",
|
|
||||||
desc: "Self-hosted broker. SSO, shared presence, team audit log, 25 peers.",
|
|
||||||
price: "$99",
|
|
||||||
note: "per month · unlimited peers",
|
|
||||||
cta: "Start free",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Business",
|
|
||||||
desc: "Multi-region brokers, retention controls, Slack/Linear bridges.",
|
|
||||||
price: "$499",
|
|
||||||
note: "per month",
|
|
||||||
cta: "Start free",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Enterprise",
|
|
||||||
desc: "Air-gapped deploy, custom SAML, dedicated support, SOC 2 pack.",
|
|
||||||
price: "Contact",
|
|
||||||
cta: "Contact sales",
|
|
||||||
href: "/contact",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Pricing = () => {
|
export const Pricing = () => {
|
||||||
const [tab, setTab] = useState<"individual" | "team">("individual");
|
|
||||||
const tiers = TIERS[tab];
|
|
||||||
return (
|
return (
|
||||||
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
||||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||||
@@ -73,72 +34,104 @@ export const Pricing = () => {
|
|||||||
Get started with claudemesh
|
Get started with claudemesh
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={2} className="mt-10 flex justify-center">
|
<Reveal delay={2}>
|
||||||
<div className="inline-flex rounded-[var(--cm-radius-xs)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-1">
|
<p
|
||||||
{(["individual", "team"] as const).map((k) => (
|
className="mx-auto mt-4 max-w-[520px] text-center text-[15px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
<button
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
key={k}
|
>
|
||||||
onClick={() => setTab(k)}
|
Free during public beta. The CLI is MIT-licensed. The hosted
|
||||||
className={
|
broker stays free while the roadmap ships. No billing today.
|
||||||
"rounded-[calc(var(--cm-radius-xs)-2px)] px-4 py-2 text-[13px] font-medium transition-colors " +
|
</p>
|
||||||
(tab === k
|
</Reveal>
|
||||||
? "bg-[var(--cm-fg)] text-[var(--cm-bg)]"
|
|
||||||
: "text-[var(--cm-fg-secondary)] hover:text-[var(--cm-fg)]")
|
<Reveal delay={3}>
|
||||||
}
|
<div className="mx-auto mt-16 max-w-[720px] rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-8 md:p-10">
|
||||||
|
<div className="mb-6 flex items-baseline justify-between gap-4">
|
||||||
|
<h3
|
||||||
|
className="text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
Public beta
|
||||||
|
</h3>
|
||||||
|
<div className="text-right">
|
||||||
|
<div
|
||||||
|
className="text-[32px] font-medium text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
Free
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-xs text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
no card required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="mb-3 text-[10px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
Shipping today
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{SHIPPING.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex items-start gap-2 text-[13px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<span className="mt-[6px] block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--cm-clay)]" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="mb-3 text-[10px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
Roadmap · v0.2–v0.3
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{ROADMAP.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex items-start gap-2 text-[13px] leading-[1.6] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<span className="mt-[6px] block h-[6px] w-[6px] shrink-0 rounded-full border border-[var(--cm-fg-tertiary)]" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-start gap-3 border-t border-[var(--cm-border)] pt-6 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p
|
||||||
|
className="text-[12px] leading-[1.5] text-[var(--cm-fg-tertiary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
{k === "individual" ? "Individual" : "Team & Enterprise"}
|
Paid tiers launch when the dashboard ships. Beta users keep
|
||||||
</button>
|
the free plan for life.
|
||||||
))}
|
</p>
|
||||||
</div>
|
<Link
|
||||||
</Reveal>
|
href="/auth/register"
|
||||||
<Reveal delay={3}>
|
className="inline-flex shrink-0 items-center gap-2 rounded-[var(--cm-radius-xs)] bg-[var(--cm-fg)] px-5 py-2.5 text-sm font-medium text-[var(--cm-bg)] transition-colors hover:bg-[var(--cm-gray-150)]"
|
||||||
<div className="mt-16 grid gap-6 md:grid-cols-3">
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
{tiers.map((tier) => (
|
|
||||||
<article
|
|
||||||
key={tier.name}
|
|
||||||
className="flex flex-col rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-8 transition-colors hover:border-[var(--cm-clay)]"
|
|
||||||
>
|
>
|
||||||
<div className="mb-5">
|
Start free
|
||||||
<SectionIcon glyph="leaf" />
|
<span className="transition-transform duration-300 group-hover:translate-x-0.5">
|
||||||
</div>
|
→
|
||||||
<h3
|
</span>
|
||||||
className="mb-2 text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
</Link>
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
</div>
|
||||||
>
|
|
||||||
{tier.name}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className="mb-6 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{tier.desc}
|
|
||||||
</p>
|
|
||||||
<div className="mb-6 mt-auto">
|
|
||||||
<div
|
|
||||||
className="text-[32px] font-medium text-[var(--cm-fg)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{tier.price}
|
|
||||||
</div>
|
|
||||||
{tier.note && (
|
|
||||||
<div
|
|
||||||
className="text-xs text-[var(--cm-fg-tertiary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
|
||||||
>
|
|
||||||
{tier.note}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={tier.href}
|
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
|
||||||
>
|
|
||||||
{tier.cta}
|
|
||||||
</Link>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const Surfaces = () => {
|
|||||||
name, by repo, by priority.
|
name, by repo, by priority.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="/dashboard"
|
||||||
className="inline-flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
className="inline-flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const NEWS = [
|
const NEWS = [
|
||||||
|
{
|
||||||
|
tag: "New",
|
||||||
|
title: "claudemesh launch (v0.1.2)",
|
||||||
|
body: "Real-time peer messages pushed into Claude Code mid-turn. One command. Source open at github.com/alezmad/claudemesh-cli.",
|
||||||
|
href: "https://github.com/alezmad/claudemesh-cli",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tag: "Beta",
|
tag: "Beta",
|
||||||
title: "Mesh Dashboard",
|
title: "Mesh Dashboard",
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ const USE_CASES: UseCase[] = [
|
|||||||
title: "Bug Alice fixed, Bob rediscovers",
|
title: "Bug Alice fixed, Bob rediscovers",
|
||||||
before:
|
before:
|
||||||
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
||||||
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude self-nominates with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude surfaces the history on its own.",
|
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude volunteers with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude shares the history on its own.",
|
||||||
limits:
|
limits:
|
||||||
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export const proxy = (request: NextRequest) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: "/((?!api|static|.*\\..*|_next).*)",
|
matcher: "/((?!api|static|install|.*\\..*|_next).*)",
|
||||||
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user