Compare commits
2 Commits
f8369a0e9b
...
cli-v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ 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";
|
||||||
|
|
||||||
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
const HELP = `claudemesh — peer mesh for Claude Code sessions
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ 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
|
||||||
@@ -55,6 +59,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;
|
||||||
|
|||||||
@@ -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.2" },
|
||||||
{
|
{
|
||||||
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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user