diff --git a/apps/cli/README.md b/apps/cli/README.md index 0d9f1db..bb1efc1 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code. claudemesh join https://claudemesh.com/join/ ``` +## Launch Claude Code + +For real-time **push messages** from peers (messages injected mid-turn +as `` 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 # join a mesh via invite URL claudemesh list # show joined meshes + identities claudemesh leave # leave a mesh diff --git a/apps/cli/package.json b/apps/cli/package.json index 881eddf..9c8b4c3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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", diff --git a/apps/cli/src/commands/install.ts b/apps/cli/src/commands/install.ts index f0ad089..cdd4fa4 100644 --- a/apps/cli/src/commands/install.ts +++ b/apps/cli/src/commands/install.ts @@ -307,6 +307,17 @@ export function runInstall(args: string[] = []): void { console.log( `Next: ${bold("claudemesh join https://claudemesh.com/join/")}`, ); + 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 { diff --git a/apps/cli/src/commands/launch.ts b/apps/cli/src/commands/launch.ts new file mode 100644 index 0000000..3e83c71 --- /dev/null +++ b/apps/cli/src/commands/launch.ts @@ -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 ` 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 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); + }); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8cf5d83..1956bb4 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -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 Join a mesh via https://claudemesh.com/join/... URL list Show all joined meshes leave Leave a joined mesh @@ -55,6 +59,9 @@ async function main(): Promise { case "hook": await runHook(args); return; + case "launch": + runLaunch(args); + return; case "join": await runJoin(args); return;