Compare commits
3 Commits
f8369a0e9b
...
fa234fae25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa234fae25 | ||
|
|
7ab3c8d465 | ||
|
|
f144e0485a |
@@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code.
|
||||
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
|
||||
mesh id, expiry, signing key, and role. Your CLI verifies it,
|
||||
generates a fresh keypair, enrolls you with the broker, and persists
|
||||
@@ -36,7 +58,9 @@ the result to `~/.claudemesh/config.json`.
|
||||
## Commands
|
||||
|
||||
```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 list # show joined meshes + identities
|
||||
claudemesh leave <slug> # leave a mesh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -307,6 +307,17 @@ export function runInstall(args: string[] = []): void {
|
||||
console.log(
|
||||
`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 {
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { runList } from "./commands/list";
|
||||
import { runLeave } from "./commands/leave";
|
||||
import { runSeedTestMesh } from "./commands/seed-test-mesh";
|
||||
import { runHook } from "./commands/hook";
|
||||
import { runLaunch } from "./commands/launch";
|
||||
|
||||
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
||||
|
||||
@@ -25,6 +26,9 @@ Commands:
|
||||
install Register MCP + Stop/UserPromptSubmit status hooks
|
||||
(add --no-hooks for bare MCP registration)
|
||||
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
|
||||
list Show all joined meshes
|
||||
leave <slug> Leave a joined mesh
|
||||
@@ -55,6 +59,9 @@ async function main(): Promise<void> {
|
||||
case "hook":
|
||||
await runHook(args);
|
||||
return;
|
||||
case "launch":
|
||||
runLaunch(args);
|
||||
return;
|
||||
case "join":
|
||||
await runJoin(args);
|
||||
return;
|
||||
|
||||
@@ -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 {
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -82,7 +87,7 @@ export async function startMcpServer(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
|
||||
const server = new Server(
|
||||
{ name: "claudemesh", version: "0.1.1" },
|
||||
{ name: "claudemesh", version: "0.1.2" },
|
||||
{
|
||||
capabilities: {
|
||||
experimental: { "claude/channel": {} },
|
||||
@@ -215,7 +220,7 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
||||
const fromName = fromPubkey
|
||||
? `peer-${fromPubkey.slice(0, 8)}`
|
||||
: "unknown";
|
||||
const content = msg.plaintext ?? "(decryption failed)";
|
||||
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
||||
try {
|
||||
await server.notification({
|
||||
method: "notifications/claude/channel",
|
||||
|
||||
@@ -312,10 +312,14 @@ export class BrokerClient {
|
||||
this.mesh.secretKey,
|
||||
);
|
||||
}
|
||||
// If decryption failed, fall back to base64 UTF-8 unwrap —
|
||||
// this covers the legacy plaintext path for broadcasts/channels
|
||||
// until channel crypto lands.
|
||||
if (plaintext === null && ciphertext) {
|
||||
// Legacy/broadcast path: no senderPubkey means the message
|
||||
// was not crypto_box'd, so base64 UTF-8 unwrap is correct.
|
||||
// For direct messages (senderPubkey present) we MUST NOT
|
||||
// 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 {
|
||||
plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
|
||||
} catch {
|
||||
|
||||
@@ -3,6 +3,12 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
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",
|
||||
title: "Mesh Dashboard",
|
||||
|
||||
Reference in New Issue
Block a user