claudemesh-cli 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/index.js +237 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -55340,13 +55340,28 @@ ${body}`;
55340
55340
  }
55341
55341
  async function startMcpServer() {
55342
55342
  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.
55343
+ const server = new Server({ name: "claudemesh", version: "0.1.1" }, {
55344
+ capabilities: {
55345
+ experimental: { "claude/channel": {} },
55346
+ tools: {}
55347
+ },
55348
+ instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions on this machine and elsewhere.
55349
+
55350
+ 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.
55351
+
55352
+ 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
55353
 
55347
- Use these tools to coordinate with peers on demand. Respond promptly when you receive messages (they're like someone tapping your shoulder).
55354
+ Available tools:
55355
+ - list_peers: see joined meshes + their connection status
55356
+ - send_message: send to a peer pubkey, channel, or broadcast (priority: now/next/low)
55357
+ - check_messages: drain buffered inbound messages (usually auto-pushed)
55358
+ - set_summary: 1-2 sentence summary of what you're working on
55359
+ - set_status: manually override your status (idle/working/dnd)
55348
55360
 
55349
- Tools: send_message, list_peers, check_messages, set_summary, set_status.
55361
+ Message priority:
55362
+ - "now": delivered immediately regardless of recipient status (use sparingly)
55363
+ - "next" (default): delivered when recipient is idle
55364
+ - "low": pull-only (check_messages)
55350
55365
 
55351
55366
  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
55367
  });
@@ -55422,6 +55437,31 @@ ${drained.join(`
55422
55437
  await startClients(config2);
55423
55438
  const transport = new StdioServerTransport;
55424
55439
  await server.connect(transport);
55440
+ for (const client of allClients()) {
55441
+ client.onPush(async (msg) => {
55442
+ const fromPubkey = msg.senderPubkey || "";
55443
+ const fromName = fromPubkey ? `peer-${fromPubkey.slice(0, 8)}` : "unknown";
55444
+ const content = msg.plaintext ?? "(decryption failed)";
55445
+ try {
55446
+ await server.notification({
55447
+ method: "notifications/claude/channel",
55448
+ params: {
55449
+ content,
55450
+ meta: {
55451
+ from_id: fromPubkey,
55452
+ from_name: fromName,
55453
+ mesh_slug: client.meshSlug,
55454
+ mesh_id: client.meshId,
55455
+ priority: msg.priority,
55456
+ sent_at: msg.createdAt,
55457
+ delivered_at: msg.receivedAt,
55458
+ kind: msg.kind
55459
+ }
55460
+ }
55461
+ });
55462
+ } catch {}
55463
+ });
55464
+ }
55425
55465
  const shutdown = () => {
55426
55466
  stopAll();
55427
55467
  process.exit(0);
@@ -55444,6 +55484,10 @@ import { fileURLToPath } from "node:url";
55444
55484
  import { spawnSync } from "node:child_process";
55445
55485
  var MCP_NAME = "claudemesh";
55446
55486
  var CLAUDE_CONFIG = join2(homedir2(), ".claude.json");
55487
+ var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
55488
+ var HOOK_COMMAND_STOP = "claudemesh hook idle";
55489
+ var HOOK_COMMAND_USER_PROMPT = "claudemesh hook working";
55490
+ var HOOK_MARKER = "claudemesh hook ";
55447
55491
  function readClaudeConfig() {
55448
55492
  if (!existsSync2(CLAUDE_CONFIG))
55449
55493
  return {};
@@ -55491,7 +55535,71 @@ function buildMcpEntry(entryPath) {
55491
55535
  function entriesEqual(a, b) {
55492
55536
  return a.command === b.command && JSON.stringify(a.args ?? []) === JSON.stringify(b.args ?? []);
55493
55537
  }
55494
- function runInstall() {
55538
+ function readClaudeSettings() {
55539
+ if (!existsSync2(CLAUDE_SETTINGS))
55540
+ return {};
55541
+ const text2 = readFileSync2(CLAUDE_SETTINGS, "utf-8").trim();
55542
+ if (!text2)
55543
+ return {};
55544
+ try {
55545
+ return JSON.parse(text2);
55546
+ } catch (e) {
55547
+ throw new Error(`failed to parse ${CLAUDE_SETTINGS}: ${e instanceof Error ? e.message : String(e)}`);
55548
+ }
55549
+ }
55550
+ function writeClaudeSettings(obj) {
55551
+ mkdirSync2(dirname2(CLAUDE_SETTINGS), { recursive: true });
55552
+ writeFileSync2(CLAUDE_SETTINGS, JSON.stringify(obj, null, 2) + `
55553
+ `, "utf-8");
55554
+ }
55555
+ function installHooks() {
55556
+ const settings = readClaudeSettings();
55557
+ const hooks = (settings.hooks ??= {}) ?? {};
55558
+ let added = 0;
55559
+ let unchanged = 0;
55560
+ const ensure = (event, command) => {
55561
+ const list = hooks[event] ??= [];
55562
+ const alreadyPresent = list.some((entry) => (entry.hooks ?? []).some((h) => h.command === command));
55563
+ if (alreadyPresent) {
55564
+ unchanged += 1;
55565
+ return;
55566
+ }
55567
+ list.push({ hooks: [{ type: "command", command }] });
55568
+ added += 1;
55569
+ };
55570
+ ensure("Stop", HOOK_COMMAND_STOP);
55571
+ ensure("UserPromptSubmit", HOOK_COMMAND_USER_PROMPT);
55572
+ settings.hooks = hooks;
55573
+ writeClaudeSettings(settings);
55574
+ return { added, unchanged };
55575
+ }
55576
+ function uninstallHooks() {
55577
+ if (!existsSync2(CLAUDE_SETTINGS))
55578
+ return 0;
55579
+ const settings = readClaudeSettings();
55580
+ const hooks = settings.hooks;
55581
+ if (!hooks)
55582
+ return 0;
55583
+ let removed = 0;
55584
+ for (const event of Object.keys(hooks)) {
55585
+ const kept = [];
55586
+ for (const entry of hooks[event] ?? []) {
55587
+ const filtered = (entry.hooks ?? []).filter((h) => !(h.command ?? "").includes(HOOK_MARKER));
55588
+ removed += (entry.hooks ?? []).length - filtered.length;
55589
+ if (filtered.length > 0)
55590
+ kept.push({ ...entry, hooks: filtered });
55591
+ }
55592
+ if (kept.length === 0)
55593
+ delete hooks[event];
55594
+ else
55595
+ hooks[event] = kept;
55596
+ }
55597
+ settings.hooks = hooks;
55598
+ writeClaudeSettings(settings);
55599
+ return removed;
55600
+ }
55601
+ function runInstall(args = []) {
55602
+ const skipHooks = args.includes("--no-hooks");
55495
55603
  console.log("claudemesh install");
55496
55604
  console.log("------------------");
55497
55605
  const entry = resolveEntry();
@@ -55534,6 +55642,22 @@ function runInstall() {
55534
55642
  console.log(`✓ MCP server "${MCP_NAME}" ${action}`);
55535
55643
  console.log(dim(` config: ${CLAUDE_CONFIG}`));
55536
55644
  console.log(dim(` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`));
55645
+ if (!skipHooks) {
55646
+ try {
55647
+ const { added, unchanged } = installHooks();
55648
+ if (added > 0) {
55649
+ console.log(`✓ Hooks registered (Stop + UserPromptSubmit) → ${added} added, ${unchanged} already present`);
55650
+ } else {
55651
+ console.log(`✓ Hooks already registered (${unchanged} present)`);
55652
+ }
55653
+ console.log(dim(` config: ${CLAUDE_SETTINGS}`));
55654
+ } catch (e) {
55655
+ console.error(`⚠ hook registration failed: ${e instanceof Error ? e.message : String(e)}`);
55656
+ console.error(" (MCP is still installed — hooks just skip. Retry with --no-hooks to suppress.)");
55657
+ }
55658
+ } else {
55659
+ console.log(dim("· Hooks skipped (--no-hooks)"));
55660
+ }
55537
55661
  console.log("");
55538
55662
  console.log(yellow(bold("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear."));
55539
55663
  console.log("");
@@ -55542,21 +55666,32 @@ function runInstall() {
55542
55666
  function runUninstall() {
55543
55667
  console.log("claudemesh uninstall");
55544
55668
  console.log("--------------------");
55545
- if (!existsSync2(CLAUDE_CONFIG)) {
55546
- console.log(`· no ${CLAUDE_CONFIG} — nothing to remove`);
55547
- return;
55669
+ if (existsSync2(CLAUDE_CONFIG)) {
55670
+ const cfg = readClaudeConfig();
55671
+ const servers = cfg.mcpServers;
55672
+ if (servers && MCP_NAME in servers) {
55673
+ delete servers[MCP_NAME];
55674
+ cfg.mcpServers = servers;
55675
+ writeClaudeConfig(cfg);
55676
+ console.log(`✓ MCP server "${MCP_NAME}" removed`);
55677
+ } else {
55678
+ console.log(`· MCP server "${MCP_NAME}" not present`);
55679
+ }
55680
+ } else {
55681
+ console.log(`· no ${CLAUDE_CONFIG} — MCP entry skipped`);
55548
55682
  }
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;
55683
+ try {
55684
+ const removed = uninstallHooks();
55685
+ if (removed > 0) {
55686
+ console.log(`✓ Hooks removed (${removed} entries)`);
55687
+ } else {
55688
+ console.log("· No claudemesh hooks to remove");
55689
+ }
55690
+ } catch (e) {
55691
+ console.error(`⚠ hook removal failed: ${e instanceof Error ? e.message : String(e)}`);
55554
55692
  }
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.");
55693
+ console.log("");
55694
+ console.log("Restart Claude Code to drop the MCP connection + hooks.");
55560
55695
  }
55561
55696
 
55562
55697
  // src/invite/parse.ts
@@ -55794,6 +55929,82 @@ function runSeedTestMesh(args) {
55794
55929
  console.log(`Run \`claudemesh mcp\` to connect, or register with Claude Code via \`claudemesh install\`.`);
55795
55930
  }
55796
55931
 
55932
+ // src/commands/hook.ts
55933
+ var DEBUG = process.env.CLAUDEMESH_HOOK_DEBUG === "1";
55934
+ function debug(msg) {
55935
+ if (DEBUG)
55936
+ console.error(`[claudemesh-hook] ${msg}`);
55937
+ }
55938
+ function wsToHttp2(wsUrl) {
55939
+ try {
55940
+ const u = new URL(wsUrl);
55941
+ const httpScheme = u.protocol === "wss:" ? "https:" : "http:";
55942
+ return `${httpScheme}//${u.host}`;
55943
+ } catch {
55944
+ return wsUrl;
55945
+ }
55946
+ }
55947
+ async function readStdinJson() {
55948
+ if (process.stdin.isTTY)
55949
+ return {};
55950
+ const chunks = [];
55951
+ const reader = process.stdin;
55952
+ try {
55953
+ for await (const chunk of reader) {
55954
+ chunks.push(chunk);
55955
+ if (chunks.reduce((n, c) => n + c.length, 0) > 256 * 1024)
55956
+ break;
55957
+ }
55958
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
55959
+ if (!raw)
55960
+ return {};
55961
+ return JSON.parse(raw);
55962
+ } catch {
55963
+ return {};
55964
+ }
55965
+ }
55966
+ async function postHook(brokerWsUrl, body) {
55967
+ const base = wsToHttp2(brokerWsUrl);
55968
+ try {
55969
+ const controller = new AbortController;
55970
+ const t = setTimeout(() => controller.abort(), 1000);
55971
+ await fetch(`${base}/hook/set-status`, {
55972
+ method: "POST",
55973
+ headers: { "Content-Type": "application/json" },
55974
+ body: JSON.stringify(body),
55975
+ signal: controller.signal
55976
+ }).finally(() => clearTimeout(t));
55977
+ } catch (e) {
55978
+ debug(`post failed ${base}: ${e instanceof Error ? e.message : e}`);
55979
+ }
55980
+ }
55981
+ async function runHook(args) {
55982
+ const status = args[0];
55983
+ if (!status || !["idle", "working", "dnd"].includes(status)) {
55984
+ process.exit(0);
55985
+ }
55986
+ const stdinTimeout = new Promise((r) => setTimeout(() => r({}), 500));
55987
+ const payload = await Promise.race([readStdinJson(), stdinTimeout]);
55988
+ const cwd = typeof payload.cwd === "string" && payload.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
55989
+ const sessionId = typeof payload.session_id === "string" && payload.session_id || "";
55990
+ let config2;
55991
+ try {
55992
+ config2 = loadConfig();
55993
+ } catch (e) {
55994
+ debug(`config load failed: ${e instanceof Error ? e.message : e}`);
55995
+ process.exit(0);
55996
+ }
55997
+ if (config2.meshes.length === 0) {
55998
+ debug("no joined meshes, nothing to do");
55999
+ process.exit(0);
56000
+ }
56001
+ const body = { cwd, pid: process.ppid, status, session_id: sessionId };
56002
+ debug(`status=${status} cwd=${cwd} meshes=${config2.meshes.length} session=${sessionId.slice(0, 8)}`);
56003
+ const brokerUrls = [...new Set(config2.meshes.map((m) => m.brokerUrl))];
56004
+ await Promise.all(brokerUrls.map((url2) => postHook(url2, body)));
56005
+ process.exit(0);
56006
+ }
56007
+
55797
56008
  // src/index.ts
55798
56009
  var HELP = `claudemesh — peer mesh for Claude Code sessions
55799
56010
 
@@ -55801,8 +56012,9 @@ Usage:
55801
56012
  claudemesh <command> [args]
55802
56013
 
55803
56014
  Commands:
55804
- install Register claudemesh as a Claude Code MCP server
55805
- uninstall Remove claudemesh MCP server registration
56015
+ install Register MCP + Stop/UserPromptSubmit status hooks
56016
+ (add --no-hooks for bare MCP registration)
56017
+ uninstall Remove MCP server + hooks
55806
56018
  join <url> Join a mesh via https://claudemesh.com/join/... URL
55807
56019
  list Show all joined meshes
55808
56020
  leave <slug> Leave a joined mesh
@@ -55823,11 +56035,14 @@ async function main() {
55823
56035
  await startMcpServer();
55824
56036
  return;
55825
56037
  case "install":
55826
- runInstall();
56038
+ runInstall(args);
55827
56039
  return;
55828
56040
  case "uninstall":
55829
56041
  runUninstall();
55830
56042
  return;
56043
+ case "hook":
56044
+ await runHook(args);
56045
+ return;
55831
56046
  case "join":
55832
56047
  await runJoin(args);
55833
56048
  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.1",
4
4
  "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
5
5
  "keywords": [
6
6
  "claude-code",