feat(cli): claudemesh launch command with transparency banner (v0.1.2)
Adds `claudemesh launch [args]` that spawns Claude Code with --dangerously-load-development-channels server:claudemesh so peer messages arrive as <channel> system reminders mid-turn instead of pull-only via check_messages. Windows uses shell:true to resolve claude.cmd from PATHEXT. Prints an info banner before spawning that explains the channel's scope (peer text injection only), the trust model (treat as untrusted input), and that existing tool-approval prompts remain the safety net. --quiet skips the banner. Install output now mentions `claudemesh launch` as the recommended launch path; plain `claude` still works for pull-only mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user