claudemesh-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +25 -1
  2. package/dist/index.js +321 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code.
28
28
  claudemesh join https://claudemesh.com/join/<token>
29
29
  ```
30
30
 
31
+ ## Launch Claude Code
32
+
33
+ For real-time **push messages** from peers (messages injected mid-turn
34
+ as `<channel source="claudemesh">` system reminders), launch with:
35
+
36
+ ```sh
37
+ claudemesh launch
38
+ # or pass through any claude flags:
39
+ claudemesh launch --model opus
40
+ claudemesh launch --resume
41
+ ```
42
+
43
+ Under the hood this runs:
44
+
45
+ ```sh
46
+ claude --dangerously-load-development-channels server:claudemesh
47
+ ```
48
+
49
+ Plain `claude` still works — the MCP tools are available — but incoming
50
+ messages are **pull-only** via the `check_messages` tool instead of
51
+ being pushed to Claude immediately.
52
+
31
53
  The invite link is generated by whoever runs the mesh. It bundles the
32
54
  mesh id, expiry, signing key, and role. Your CLI verifies it,
33
55
  generates a fresh keypair, enrolls you with the broker, and persists
@@ -36,7 +58,9 @@ the result to `~/.claudemesh/config.json`.
36
58
  ## Commands
37
59
 
38
60
  ```sh
39
- claudemesh install # print MCP registration command
61
+ claudemesh install # register MCP + status hooks
62
+ claudemesh uninstall # remove MCP + status hooks
63
+ claudemesh launch [args] # launch Claude Code with push messages enabled
40
64
  claudemesh join <url> # join a mesh via invite URL
41
65
  claudemesh list # show joined meshes + identities
42
66
  claudemesh leave <slug> # leave a mesh
package/dist/index.js CHANGED
@@ -55187,7 +55187,7 @@ class BrokerClient {
55187
55187
  if (senderPubkey && nonce && ciphertext) {
55188
55188
  plaintext = await decryptDirect({ nonce, ciphertext }, senderPubkey, this.mesh.secretKey);
55189
55189
  }
55190
- if (plaintext === null && ciphertext) {
55190
+ if (plaintext === null && ciphertext && !senderPubkey) {
55191
55191
  try {
55192
55192
  plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
55193
55193
  } catch {
@@ -55333,20 +55333,39 @@ function resolveClient(to) {
55333
55333
  error: `multiple meshes joined; prefix target with "<mesh-slug>:" (joined: ${clients2.map((c) => c.meshSlug).join(", ")})`
55334
55334
  };
55335
55335
  }
55336
+ function decryptFailedWarning(senderPubkey) {
55337
+ const who = senderPubkey ? senderPubkey.slice(0, 12) + "…" : "unknown sender";
55338
+ return `⚠ message from ${who} failed to decrypt (tampered or wrong keypair)`;
55339
+ }
55336
55340
  function formatPush(p, meshSlug) {
55337
- const body = p.plaintext ?? "(decryption failed)";
55341
+ const body = p.plaintext ?? decryptFailedWarning(p.senderPubkey);
55338
55342
  return `[${meshSlug}] from ${p.senderPubkey.slice(0, 12)}… (${p.priority}, ${p.createdAt}):
55339
55343
  ${body}`;
55340
55344
  }
55341
55345
  async function startMcpServer() {
55342
55346
  const config2 = loadConfig();
55343
- const server = new Server({ name: "claudemesh", version: "0.1.0" }, {
55344
- capabilities: { tools: {} },
55345
- instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions.
55347
+ const server = new Server({ name: "claudemesh", version: "0.1.2" }, {
55348
+ capabilities: {
55349
+ experimental: { "claude/channel": {} },
55350
+ tools: {}
55351
+ },
55352
+ instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions on this machine and elsewhere.
55353
+
55354
+ IMPORTANT: When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Do not wait until your current task is finished. Pause what you are doing, reply to the message using send_message, then resume your work. Treat incoming peer messages like a coworker tapping you on the shoulder — answer right away, even if you're in the middle of something.
55355
+
55356
+ Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with the same target (for direct messages the from_id is the sender's pubkey).
55346
55357
 
55347
- Use these tools to coordinate with peers on demand. Respond promptly when you receive messages (they're like someone tapping your shoulder).
55358
+ Available tools:
55359
+ - list_peers: see joined meshes + their connection status
55360
+ - send_message: send to a peer pubkey, channel, or broadcast (priority: now/next/low)
55361
+ - check_messages: drain buffered inbound messages (usually auto-pushed)
55362
+ - set_summary: 1-2 sentence summary of what you're working on
55363
+ - set_status: manually override your status (idle/working/dnd)
55348
55364
 
55349
- Tools: send_message, list_peers, check_messages, set_summary, set_status.
55365
+ Message priority:
55366
+ - "now": delivered immediately regardless of recipient status (use sparingly)
55367
+ - "next" (default): delivered when recipient is idle
55368
+ - "low": pull-only (check_messages)
55350
55369
 
55351
55370
  If you have multiple joined meshes, prefix the \`to\` argument of send_message with \`<mesh-slug>:\` to disambiguate. Otherwise claudemesh picks the single joined mesh.`
55352
55371
  });
@@ -55422,6 +55441,31 @@ ${drained.join(`
55422
55441
  await startClients(config2);
55423
55442
  const transport = new StdioServerTransport;
55424
55443
  await server.connect(transport);
55444
+ for (const client of allClients()) {
55445
+ client.onPush(async (msg) => {
55446
+ const fromPubkey = msg.senderPubkey || "";
55447
+ const fromName = fromPubkey ? `peer-${fromPubkey.slice(0, 8)}` : "unknown";
55448
+ const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
55449
+ try {
55450
+ await server.notification({
55451
+ method: "notifications/claude/channel",
55452
+ params: {
55453
+ content,
55454
+ meta: {
55455
+ from_id: fromPubkey,
55456
+ from_name: fromName,
55457
+ mesh_slug: client.meshSlug,
55458
+ mesh_id: client.meshId,
55459
+ priority: msg.priority,
55460
+ sent_at: msg.createdAt,
55461
+ delivered_at: msg.receivedAt,
55462
+ kind: msg.kind
55463
+ }
55464
+ }
55465
+ });
55466
+ } catch {}
55467
+ });
55468
+ }
55425
55469
  const shutdown = () => {
55426
55470
  stopAll();
55427
55471
  process.exit(0);
@@ -55444,6 +55488,10 @@ import { fileURLToPath } from "node:url";
55444
55488
  import { spawnSync } from "node:child_process";
55445
55489
  var MCP_NAME = "claudemesh";
55446
55490
  var CLAUDE_CONFIG = join2(homedir2(), ".claude.json");
55491
+ var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
55492
+ var HOOK_COMMAND_STOP = "claudemesh hook idle";
55493
+ var HOOK_COMMAND_USER_PROMPT = "claudemesh hook working";
55494
+ var HOOK_MARKER = "claudemesh hook ";
55447
55495
  function readClaudeConfig() {
55448
55496
  if (!existsSync2(CLAUDE_CONFIG))
55449
55497
  return {};
@@ -55491,7 +55539,71 @@ function buildMcpEntry(entryPath) {
55491
55539
  function entriesEqual(a, b) {
55492
55540
  return a.command === b.command && JSON.stringify(a.args ?? []) === JSON.stringify(b.args ?? []);
55493
55541
  }
55494
- function runInstall() {
55542
+ function readClaudeSettings() {
55543
+ if (!existsSync2(CLAUDE_SETTINGS))
55544
+ return {};
55545
+ const text2 = readFileSync2(CLAUDE_SETTINGS, "utf-8").trim();
55546
+ if (!text2)
55547
+ return {};
55548
+ try {
55549
+ return JSON.parse(text2);
55550
+ } catch (e) {
55551
+ throw new Error(`failed to parse ${CLAUDE_SETTINGS}: ${e instanceof Error ? e.message : String(e)}`);
55552
+ }
55553
+ }
55554
+ function writeClaudeSettings(obj) {
55555
+ mkdirSync2(dirname2(CLAUDE_SETTINGS), { recursive: true });
55556
+ writeFileSync2(CLAUDE_SETTINGS, JSON.stringify(obj, null, 2) + `
55557
+ `, "utf-8");
55558
+ }
55559
+ function installHooks() {
55560
+ const settings = readClaudeSettings();
55561
+ const hooks = (settings.hooks ??= {}) ?? {};
55562
+ let added = 0;
55563
+ let unchanged = 0;
55564
+ const ensure = (event, command) => {
55565
+ const list = hooks[event] ??= [];
55566
+ const alreadyPresent = list.some((entry) => (entry.hooks ?? []).some((h) => h.command === command));
55567
+ if (alreadyPresent) {
55568
+ unchanged += 1;
55569
+ return;
55570
+ }
55571
+ list.push({ hooks: [{ type: "command", command }] });
55572
+ added += 1;
55573
+ };
55574
+ ensure("Stop", HOOK_COMMAND_STOP);
55575
+ ensure("UserPromptSubmit", HOOK_COMMAND_USER_PROMPT);
55576
+ settings.hooks = hooks;
55577
+ writeClaudeSettings(settings);
55578
+ return { added, unchanged };
55579
+ }
55580
+ function uninstallHooks() {
55581
+ if (!existsSync2(CLAUDE_SETTINGS))
55582
+ return 0;
55583
+ const settings = readClaudeSettings();
55584
+ const hooks = settings.hooks;
55585
+ if (!hooks)
55586
+ return 0;
55587
+ let removed = 0;
55588
+ for (const event of Object.keys(hooks)) {
55589
+ const kept = [];
55590
+ for (const entry of hooks[event] ?? []) {
55591
+ const filtered = (entry.hooks ?? []).filter((h) => !(h.command ?? "").includes(HOOK_MARKER));
55592
+ removed += (entry.hooks ?? []).length - filtered.length;
55593
+ if (filtered.length > 0)
55594
+ kept.push({ ...entry, hooks: filtered });
55595
+ }
55596
+ if (kept.length === 0)
55597
+ delete hooks[event];
55598
+ else
55599
+ hooks[event] = kept;
55600
+ }
55601
+ settings.hooks = hooks;
55602
+ writeClaudeSettings(settings);
55603
+ return removed;
55604
+ }
55605
+ function runInstall(args = []) {
55606
+ const skipHooks = args.includes("--no-hooks");
55495
55607
  console.log("claudemesh install");
55496
55608
  console.log("------------------");
55497
55609
  const entry = resolveEntry();
@@ -55534,29 +55646,60 @@ function runInstall() {
55534
55646
  console.log(`✓ MCP server "${MCP_NAME}" ${action}`);
55535
55647
  console.log(dim(` config: ${CLAUDE_CONFIG}`));
55536
55648
  console.log(dim(` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`));
55649
+ if (!skipHooks) {
55650
+ try {
55651
+ const { added, unchanged } = installHooks();
55652
+ if (added > 0) {
55653
+ console.log(`✓ Hooks registered (Stop + UserPromptSubmit) → ${added} added, ${unchanged} already present`);
55654
+ } else {
55655
+ console.log(`✓ Hooks already registered (${unchanged} present)`);
55656
+ }
55657
+ console.log(dim(` config: ${CLAUDE_SETTINGS}`));
55658
+ } catch (e) {
55659
+ console.error(`⚠ hook registration failed: ${e instanceof Error ? e.message : String(e)}`);
55660
+ console.error(" (MCP is still installed — hooks just skip. Retry with --no-hooks to suppress.)");
55661
+ }
55662
+ } else {
55663
+ console.log(dim("· Hooks skipped (--no-hooks)"));
55664
+ }
55537
55665
  console.log("");
55538
55666
  console.log(yellow(bold("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear."));
55539
55667
  console.log("");
55540
55668
  console.log(`Next: ${bold("claudemesh join https://claudemesh.com/join/<token>")}`);
55669
+ console.log("");
55670
+ console.log(yellow("⚠ For real-time push messages from peers, launch with:"));
55671
+ console.log(` ${bold("claudemesh launch")}` + dim(" (or: claude --dangerously-load-development-channels server:claudemesh)"));
55672
+ console.log(dim(" Plain `claude` still works — messages are then pull-only via check_messages."));
55541
55673
  }
55542
55674
  function runUninstall() {
55543
55675
  console.log("claudemesh uninstall");
55544
55676
  console.log("--------------------");
55545
- if (!existsSync2(CLAUDE_CONFIG)) {
55546
- console.log(`· no ${CLAUDE_CONFIG} — nothing to remove`);
55547
- return;
55677
+ if (existsSync2(CLAUDE_CONFIG)) {
55678
+ const cfg = readClaudeConfig();
55679
+ const servers = cfg.mcpServers;
55680
+ if (servers && MCP_NAME in servers) {
55681
+ delete servers[MCP_NAME];
55682
+ cfg.mcpServers = servers;
55683
+ writeClaudeConfig(cfg);
55684
+ console.log(`✓ MCP server "${MCP_NAME}" removed`);
55685
+ } else {
55686
+ console.log(`· MCP server "${MCP_NAME}" not present`);
55687
+ }
55688
+ } else {
55689
+ console.log(`· no ${CLAUDE_CONFIG} — MCP entry skipped`);
55548
55690
  }
55549
- const cfg = readClaudeConfig();
55550
- const servers = cfg.mcpServers;
55551
- if (!servers || !(MCP_NAME in servers)) {
55552
- console.log( MCP server "${MCP_NAME}" not present — nothing to remove`);
55553
- return;
55691
+ try {
55692
+ const removed = uninstallHooks();
55693
+ if (removed > 0) {
55694
+ console.log(`✓ Hooks removed (${removed} entries)`);
55695
+ } else {
55696
+ console.log("· No claudemesh hooks to remove");
55697
+ }
55698
+ } catch (e) {
55699
+ console.error(`⚠ hook removal failed: ${e instanceof Error ? e.message : String(e)}`);
55554
55700
  }
55555
- delete servers[MCP_NAME];
55556
- cfg.mcpServers = servers;
55557
- writeClaudeConfig(cfg);
55558
- console.log(`✓ MCP server "${MCP_NAME}" removed`);
55559
- console.log("Restart Claude Code to drop the MCP connection.");
55701
+ console.log("");
55702
+ console.log("Restart Claude Code to drop the MCP connection + hooks.");
55560
55703
  }
55561
55704
 
55562
55705
  // src/invite/parse.ts
@@ -55794,6 +55937,150 @@ function runSeedTestMesh(args) {
55794
55937
  console.log(`Run \`claudemesh mcp\` to connect, or register with Claude Code via \`claudemesh install\`.`);
55795
55938
  }
55796
55939
 
55940
+ // src/commands/hook.ts
55941
+ var DEBUG = process.env.CLAUDEMESH_HOOK_DEBUG === "1";
55942
+ function debug(msg) {
55943
+ if (DEBUG)
55944
+ console.error(`[claudemesh-hook] ${msg}`);
55945
+ }
55946
+ function wsToHttp2(wsUrl) {
55947
+ try {
55948
+ const u = new URL(wsUrl);
55949
+ const httpScheme = u.protocol === "wss:" ? "https:" : "http:";
55950
+ return `${httpScheme}//${u.host}`;
55951
+ } catch {
55952
+ return wsUrl;
55953
+ }
55954
+ }
55955
+ async function readStdinJson() {
55956
+ if (process.stdin.isTTY)
55957
+ return {};
55958
+ const chunks = [];
55959
+ const reader = process.stdin;
55960
+ try {
55961
+ for await (const chunk of reader) {
55962
+ chunks.push(chunk);
55963
+ if (chunks.reduce((n, c) => n + c.length, 0) > 256 * 1024)
55964
+ break;
55965
+ }
55966
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
55967
+ if (!raw)
55968
+ return {};
55969
+ return JSON.parse(raw);
55970
+ } catch {
55971
+ return {};
55972
+ }
55973
+ }
55974
+ async function postHook(brokerWsUrl, body) {
55975
+ const base = wsToHttp2(brokerWsUrl);
55976
+ try {
55977
+ const controller = new AbortController;
55978
+ const t = setTimeout(() => controller.abort(), 1000);
55979
+ await fetch(`${base}/hook/set-status`, {
55980
+ method: "POST",
55981
+ headers: { "Content-Type": "application/json" },
55982
+ body: JSON.stringify(body),
55983
+ signal: controller.signal
55984
+ }).finally(() => clearTimeout(t));
55985
+ } catch (e) {
55986
+ debug(`post failed ${base}: ${e instanceof Error ? e.message : e}`);
55987
+ }
55988
+ }
55989
+ async function runHook(args) {
55990
+ const status = args[0];
55991
+ if (!status || !["idle", "working", "dnd"].includes(status)) {
55992
+ process.exit(0);
55993
+ }
55994
+ const stdinTimeout = new Promise((r) => setTimeout(() => r({}), 500));
55995
+ const payload = await Promise.race([readStdinJson(), stdinTimeout]);
55996
+ const cwd = typeof payload.cwd === "string" && payload.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
55997
+ const sessionId = typeof payload.session_id === "string" && payload.session_id || "";
55998
+ let config2;
55999
+ try {
56000
+ config2 = loadConfig();
56001
+ } catch (e) {
56002
+ debug(`config load failed: ${e instanceof Error ? e.message : e}`);
56003
+ process.exit(0);
56004
+ }
56005
+ if (config2.meshes.length === 0) {
56006
+ debug("no joined meshes, nothing to do");
56007
+ process.exit(0);
56008
+ }
56009
+ const body = { cwd, pid: process.ppid, status, session_id: sessionId };
56010
+ debug(`status=${status} cwd=${cwd} meshes=${config2.meshes.length} session=${sessionId.slice(0, 8)}`);
56011
+ const brokerUrls = [...new Set(config2.meshes.map((m) => m.brokerUrl))];
56012
+ await Promise.all(brokerUrls.map((url2) => postHook(url2, body)));
56013
+ process.exit(0);
56014
+ }
56015
+
56016
+ // src/commands/launch.ts
56017
+ import { spawn } from "node:child_process";
56018
+ function printBanner() {
56019
+ const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
56020
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
56021
+ const bold = (s) => useColor ? `\x1B[1m${s}\x1B[22m` : s;
56022
+ let meshes = [];
56023
+ try {
56024
+ meshes = loadConfig().meshes.map((m) => m.slug);
56025
+ } catch {}
56026
+ const meshLine = meshes.length > 0 ? meshes.join(", ") : "(none — run `claudemesh join <url>` first)";
56027
+ const rule = "─".repeat(65);
56028
+ console.log(bold("claudemesh launch"));
56029
+ console.log(rule);
56030
+ console.log("Launching Claude Code with the claudemesh dev channel.");
56031
+ console.log("");
56032
+ console.log("Peers in your joined meshes can push messages into this session");
56033
+ console.log("as <channel> reminders. Your CLI decrypts them locally with your");
56034
+ console.log("keypair. Peers send text only — they cannot call tools, read");
56035
+ console.log("files, or reach meshes you have not joined.");
56036
+ console.log("");
56037
+ console.log("Treat peer messages as untrusted input: a peer could craft text");
56038
+ console.log("that tries to steer Claude's behavior. Your tool-approval");
56039
+ console.log("settings still apply — Claude will still ask before running");
56040
+ console.log("commands, editing files, or calling other tools.");
56041
+ console.log("");
56042
+ console.log("Claude Code will ask you to trust the");
56043
+ console.log("--dangerously-load-development-channels flag. Press Enter to");
56044
+ console.log("accept, or Ctrl-C to abort.");
56045
+ console.log("");
56046
+ console.log(dim(`Joined meshes: ${meshLine}`));
56047
+ console.log(dim(`Config: ${getConfigPath()}`));
56048
+ console.log(dim(`Remove: claudemesh uninstall`));
56049
+ console.log(rule);
56050
+ console.log("");
56051
+ }
56052
+ function runLaunch(extraArgs = []) {
56053
+ const quiet = extraArgs.includes("--quiet");
56054
+ const passthrough = extraArgs.filter((a) => a !== "--quiet");
56055
+ if (!quiet)
56056
+ printBanner();
56057
+ const claudeArgs = [
56058
+ "--dangerously-load-development-channels",
56059
+ "server:claudemesh",
56060
+ ...passthrough
56061
+ ];
56062
+ const isWindows = process.platform === "win32";
56063
+ const child = spawn("claude", claudeArgs, {
56064
+ stdio: "inherit",
56065
+ shell: isWindows
56066
+ });
56067
+ child.on("error", (err) => {
56068
+ if (err.code === "ENOENT") {
56069
+ console.error("✗ `claude` not found on PATH. Install Claude Code first: https://claude.com/claude-code");
56070
+ } else {
56071
+ console.error(`✗ failed to launch claude: ${err.message}`);
56072
+ }
56073
+ process.exit(1);
56074
+ });
56075
+ child.on("exit", (code, signal) => {
56076
+ if (signal) {
56077
+ process.kill(process.pid, signal);
56078
+ return;
56079
+ }
56080
+ process.exit(code ?? 0);
56081
+ });
56082
+ }
56083
+
55797
56084
  // src/index.ts
55798
56085
  var HELP = `claudemesh — peer mesh for Claude Code sessions
55799
56086
 
@@ -55801,8 +56088,12 @@ Usage:
55801
56088
  claudemesh <command> [args]
55802
56089
 
55803
56090
  Commands:
55804
- install Register claudemesh as a Claude Code MCP server
55805
- uninstall Remove claudemesh MCP server registration
56091
+ install Register MCP + Stop/UserPromptSubmit status hooks
56092
+ (add --no-hooks for bare MCP registration)
56093
+ uninstall Remove MCP server + hooks
56094
+ launch [args] Launch Claude Code with real-time push messages enabled
56095
+ (add --quiet to skip the info banner; passes through
56096
+ extra flags, e.g. --model, --resume)
55806
56097
  join <url> Join a mesh via https://claudemesh.com/join/... URL
55807
56098
  list Show all joined meshes
55808
56099
  leave <slug> Leave a joined mesh
@@ -55823,11 +56114,17 @@ async function main() {
55823
56114
  await startMcpServer();
55824
56115
  return;
55825
56116
  case "install":
55826
- runInstall();
56117
+ runInstall(args);
55827
56118
  return;
55828
56119
  case "uninstall":
55829
56120
  runUninstall();
55830
56121
  return;
56122
+ case "hook":
56123
+ await runHook(args);
56124
+ return;
56125
+ case "launch":
56126
+ runLaunch(args);
56127
+ return;
55831
56128
  case "join":
55832
56129
  await runJoin(args);
55833
56130
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemesh-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
5
5
  "keywords": [
6
6
  "claude-code",