Compare commits
4 Commits
f8369a0e9b
...
cli-v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8810aa1e9e | ||
|
|
fa234fae25 | ||
|
|
7ab3c8d465 | ||
|
|
f144e0485a |
@@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code.
|
|||||||
claudemesh join https://claudemesh.com/join/<token>
|
claudemesh join https://claudemesh.com/join/<token>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Launch Claude Code
|
||||||
|
|
||||||
|
For real-time **push messages** from peers (messages injected mid-turn
|
||||||
|
as `<channel source="claudemesh">` system reminders), launch with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claudemesh launch
|
||||||
|
# or pass through any claude flags:
|
||||||
|
claudemesh launch --model opus
|
||||||
|
claudemesh launch --resume
|
||||||
|
```
|
||||||
|
|
||||||
|
Under the hood this runs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude --dangerously-load-development-channels server:claudemesh
|
||||||
|
```
|
||||||
|
|
||||||
|
Plain `claude` still works — the MCP tools are available — but incoming
|
||||||
|
messages are **pull-only** via the `check_messages` tool instead of
|
||||||
|
being pushed to Claude immediately.
|
||||||
|
|
||||||
The invite link is generated by whoever runs the mesh. It bundles the
|
The invite link is generated by whoever runs the mesh. It bundles the
|
||||||
mesh id, expiry, signing key, and role. Your CLI verifies it,
|
mesh id, expiry, signing key, and role. Your CLI verifies it,
|
||||||
generates a fresh keypair, enrolls you with the broker, and persists
|
generates a fresh keypair, enrolls you with the broker, and persists
|
||||||
@@ -36,7 +58,9 @@ the result to `~/.claudemesh/config.json`.
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claudemesh install # print MCP registration command
|
claudemesh install # register MCP + status hooks
|
||||||
|
claudemesh uninstall # remove MCP + status hooks
|
||||||
|
claudemesh launch [args] # launch Claude Code with push messages enabled
|
||||||
claudemesh join <url> # join a mesh via invite URL
|
claudemesh join <url> # join a mesh via invite URL
|
||||||
claudemesh list # show joined meshes + identities
|
claudemesh list # show joined meshes + identities
|
||||||
claudemesh leave <slug> # leave a mesh
|
claudemesh leave <slug> # leave a mesh
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -307,6 +307,17 @@ export function runInstall(args: string[] = []): void {
|
|||||||
console.log(
|
console.log(
|
||||||
`Next: ${bold("claudemesh join https://claudemesh.com/join/<token>")}`,
|
`Next: ${bold("claudemesh join https://claudemesh.com/join/<token>")}`,
|
||||||
);
|
);
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
yellow("⚠ For real-time push messages from peers, launch with:"),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${bold("claudemesh launch")}` +
|
||||||
|
dim(" (or: claude --dangerously-load-development-channels server:claudemesh)"),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
dim(" Plain `claude` still works — messages are then pull-only via check_messages."),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runUninstall(): void {
|
export function runUninstall(): void {
|
||||||
|
|||||||
94
apps/cli/src/commands/launch.ts
Normal file
94
apps/cli/src/commands/launch.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* `claudemesh launch` — spawn `claude` with the dev-channel flag so the
|
||||||
|
* claudemesh MCP server's `notifications/claude/channel` pushes get
|
||||||
|
* injected as system reminders mid-turn.
|
||||||
|
*
|
||||||
|
* Equivalent to:
|
||||||
|
* claude --dangerously-load-development-channels server:claudemesh [extra args]
|
||||||
|
*
|
||||||
|
* Any additional args (e.g. --model opus, --resume, -c) are passed
|
||||||
|
* through verbatim. Use --quiet to skip the informational banner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { loadConfig, getConfigPath } from "../state/config";
|
||||||
|
|
||||||
|
function printBanner(): 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 bold = (s: string): string => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||||
|
|
||||||
|
let meshes: string[] = [];
|
||||||
|
try {
|
||||||
|
meshes = loadConfig().meshes.map((m) => m.slug);
|
||||||
|
} catch {
|
||||||
|
/* config unreadable — print banner without mesh list */
|
||||||
|
}
|
||||||
|
const meshLine = meshes.length > 0 ? meshes.join(", ") : "(none — run `claudemesh join <url>` first)";
|
||||||
|
|
||||||
|
const rule = "─".repeat(65);
|
||||||
|
console.log(bold("claudemesh launch"));
|
||||||
|
console.log(rule);
|
||||||
|
console.log("Launching Claude Code with the claudemesh dev channel.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Peers in your joined meshes can push messages into this session");
|
||||||
|
console.log("as <channel> reminders. Your CLI decrypts them locally with your");
|
||||||
|
console.log("keypair. Peers send text only — they cannot call tools, read");
|
||||||
|
console.log("files, or reach meshes you have not joined.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Treat peer messages as untrusted input: a peer could craft text");
|
||||||
|
console.log("that tries to steer Claude's behavior. Your tool-approval");
|
||||||
|
console.log("settings still apply — Claude will still ask before running");
|
||||||
|
console.log("commands, editing files, or calling other tools.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Claude Code will ask you to trust the");
|
||||||
|
console.log("--dangerously-load-development-channels flag. Press Enter to");
|
||||||
|
console.log("accept, or Ctrl-C to abort.");
|
||||||
|
console.log("");
|
||||||
|
console.log(dim(`Joined meshes: ${meshLine}`));
|
||||||
|
console.log(dim(`Config: ${getConfigPath()}`));
|
||||||
|
console.log(dim(`Remove: claudemesh uninstall`));
|
||||||
|
console.log(rule);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runLaunch(extraArgs: string[] = []): void {
|
||||||
|
const quiet = extraArgs.includes("--quiet");
|
||||||
|
const passthrough = extraArgs.filter((a) => a !== "--quiet");
|
||||||
|
|
||||||
|
if (!quiet) printBanner();
|
||||||
|
|
||||||
|
const claudeArgs = [
|
||||||
|
"--dangerously-load-development-channels",
|
||||||
|
"server:claudemesh",
|
||||||
|
...passthrough,
|
||||||
|
];
|
||||||
|
// Windows: npm global binaries are .cmd shims. Node's spawn without
|
||||||
|
// shell:true does not resolve PATHEXT, so we need shell:true on win32
|
||||||
|
// to find claude.cmd. POSIX stays shell-less to avoid quoting surprises.
|
||||||
|
const isWindows = process.platform === "win32";
|
||||||
|
const child = spawn("claude", claudeArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: isWindows,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
console.error(
|
||||||
|
"✗ `claude` not found on PATH. Install Claude Code first: https://claude.com/claude-code",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ failed to launch claude: ${err.message}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
process.kill(process.pid, signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,12 @@ import { runList } from "./commands/list";
|
|||||||
import { runLeave } from "./commands/leave";
|
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 { runStatus } from "./commands/status";
|
||||||
|
import { runDoctor } from "./commands/doctor";
|
||||||
|
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]
|
||||||
@@ -25,12 +29,18 @@ Commands:
|
|||||||
install Register MCP + Stop/UserPromptSubmit status hooks
|
install Register MCP + Stop/UserPromptSubmit status hooks
|
||||||
(add --no-hooks for bare MCP registration)
|
(add --no-hooks for bare MCP registration)
|
||||||
uninstall Remove MCP server + hooks
|
uninstall Remove MCP server + hooks
|
||||||
|
launch [args] Launch Claude Code with real-time push messages enabled
|
||||||
|
(add --quiet to skip the info banner; passes through
|
||||||
|
extra flags, e.g. --model, --resume)
|
||||||
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)
|
||||||
@@ -55,6 +65,9 @@ async function main(): Promise<void> {
|
|||||||
case "hook":
|
case "hook":
|
||||||
await runHook(args);
|
await runHook(args);
|
||||||
return;
|
return;
|
||||||
|
case "launch":
|
||||||
|
runLaunch(args);
|
||||||
|
return;
|
||||||
case "join":
|
case "join":
|
||||||
await runJoin(args);
|
await runJoin(args);
|
||||||
return;
|
return;
|
||||||
@@ -64,9 +77,20 @@ 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":
|
||||||
|
|||||||
@@ -73,8 +73,13 @@ function resolveClient(to: string): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decryptFailedWarning(senderPubkey: string): string {
|
||||||
|
const who = senderPubkey ? senderPubkey.slice(0, 12) + "…" : "unknown sender";
|
||||||
|
return `⚠ message from ${who} failed to decrypt (tampered or wrong keypair)`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatPush(p: InboundPush, meshSlug: string): string {
|
function formatPush(p: InboundPush, meshSlug: string): string {
|
||||||
const body = p.plaintext ?? "(decryption failed)";
|
const body = p.plaintext ?? decryptFailedWarning(p.senderPubkey);
|
||||||
return `[${meshSlug}] from ${p.senderPubkey.slice(0, 12)}… (${p.priority}, ${p.createdAt}):\n${body}`;
|
return `[${meshSlug}] from ${p.senderPubkey.slice(0, 12)}… (${p.priority}, ${p.createdAt}):\n${body}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,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.1" },
|
{ name: "claudemesh", version: "0.1.3" },
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
experimental: { "claude/channel": {} },
|
experimental: { "claude/channel": {} },
|
||||||
@@ -215,7 +220,7 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
const fromName = fromPubkey
|
const fromName = fromPubkey
|
||||||
? `peer-${fromPubkey.slice(0, 8)}`
|
? `peer-${fromPubkey.slice(0, 8)}`
|
||||||
: "unknown";
|
: "unknown";
|
||||||
const content = msg.plaintext ?? "(decryption failed)";
|
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
||||||
try {
|
try {
|
||||||
await server.notification({
|
await server.notification({
|
||||||
method: "notifications/claude/channel",
|
method: "notifications/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;
|
||||||
@@ -312,10 +312,14 @@ export class BrokerClient {
|
|||||||
this.mesh.secretKey,
|
this.mesh.secretKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// If decryption failed, fall back to base64 UTF-8 unwrap —
|
// Legacy/broadcast path: no senderPubkey means the message
|
||||||
// this covers the legacy plaintext path for broadcasts/channels
|
// was not crypto_box'd, so base64 UTF-8 unwrap is correct.
|
||||||
// until channel crypto lands.
|
// For direct messages (senderPubkey present) we MUST NOT
|
||||||
if (plaintext === null && ciphertext) {
|
// base64-decode the ciphertext on decrypt failure — that
|
||||||
|
// produces garbage binary that surfaces as garbled bytes
|
||||||
|
// to Claude. Leave plaintext=null and let consumers emit
|
||||||
|
// a clear "failed to decrypt" warning.
|
||||||
|
if (plaintext === null && ciphertext && !senderPubkey) {
|
||||||
try {
|
try {
|
||||||
plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
|
plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user