claudemesh-cli 1.8.0 → 1.9.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.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Peer mesh for Claude Code sessions. Connect multiple Claude Code instances into a shared mesh with real-time messaging, shared state, memory, file sharing, vector store, scheduled jobs, and more — all driven from the `claudemesh` CLI. The MCP server is a tool-less push-pipe that delivers inbound peer messages to Claude as `<channel>` interrupts; everything else lives behind CLI verbs that Claude learns from the auto-installed `claudemesh` skill.
4
4
 
5
- > **What's new in 1.8.0:** per-topic end-to-end encryption (v0.3.0 phase 3, CLI side). `claudemesh topic post <topic> <msg>` encrypts the body with `crypto_secretbox` under the topic's symmetric key broker stores ciphertext only. `claudemesh topic tail` now decrypts v2 messages on render and runs a background re-seal loop every 30s, so new topic joiners get their sealed keys without manual action. `topic-key` cache is process-only kill the CLI, the key forgets. Web dashboard reads v1 plaintext for now (phase 3.5 brings browser-side identity).
5
+ > **What's new in 1.9.x:** topic threading + multi-session reliability fixes. `claudemesh topic post <topic> <msg> --reply-to <id>` threads a reply onto a previous topic message (full id or 8+ char prefix); `topic tail` renders `↳ in reply to <name>: "<snippet>"` above replies and shows a copyable `#xxxxxxxx` short id on every row. `<channel>` MCP attrs now carry `from_member_id`, `from_pubkey` (stable), `from_session_pubkey` (ephemeral), `message_id`, `topic`, `reply_to_id` everything the recipient needs to reply directly. Broker fixes (v0.3.2): replies to a stale session pubkey now resolve to the owning member's live session instead of bouncing with "not online", and broadcast `*` no longer loopbacks decrypt-fail warnings to the sender's sibling sessions.
6
+ >
7
+ > **What was new in 1.8.0:** per-topic end-to-end encryption (v0.3.0 phase 3, CLI side). `claudemesh topic post <topic> <msg>` encrypts the body with `crypto_secretbox` under the topic's symmetric key — broker stores ciphertext only. `claudemesh topic tail` now decrypts v2 messages on render and runs a background re-seal loop every 30s, so new topic joiners get their sealed keys without manual action. `topic-key` cache is process-only — kill the CLI, the key forgets. Web dashboard reads v1 plaintext for now (phase 3.5 brings browser-side identity).
6
8
  >
7
9
  > **What was new in 1.7.0:** terminal parity for the v1.6.x server features. New verbs: `claudemesh topic tail` (live SSE message stream — Ctrl-C to exit), `claudemesh notification list` (recent `@you` mentions across topics), `claudemesh member list` (mesh roster with online dots, distinct from `peer list`'s live-session view). Each command auto-mints a 5-minute read-only apikey via the WebSocket and revokes it on exit, so no token plumbing is needed.
8
10
  >
@@ -88,7 +88,7 @@ __export(exports_urls, {
88
88
  VERSION: () => VERSION,
89
89
  URLS: () => URLS
90
90
  });
91
- var URLS, VERSION = "1.8.0", env;
91
+ var URLS, VERSION = "1.9.2", env;
92
92
  var init_urls = __esm(() => {
93
93
  URLS = {
94
94
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -2779,6 +2779,11 @@ class BrokerClient {
2779
2779
  messageId: String(msg.messageId ?? ""),
2780
2780
  meshId: String(msg.meshId ?? ""),
2781
2781
  senderPubkey,
2782
+ ...msg.senderMemberPubkey ? { senderMemberPubkey: String(msg.senderMemberPubkey) } : {},
2783
+ ...msg.senderMemberId ? { senderMemberId: String(msg.senderMemberId) } : {},
2784
+ ...msg.senderName ? { senderName: String(msg.senderName) } : {},
2785
+ ...msg.topic ? { topic: String(msg.topic) } : {},
2786
+ ...msg.replyToId ? { replyToId: String(msg.replyToId) } : {},
2782
2787
  priority: msg.priority ?? "next",
2783
2788
  nonce,
2784
2789
  ciphertext,
@@ -11798,6 +11803,17 @@ var exports_topic_tail = {};
11798
11803
  __export(exports_topic_tail, {
11799
11804
  runTopicTail: () => runTopicTail
11800
11805
  });
11806
+ function rememberRendered(cache2, m, text) {
11807
+ cache2.set(m.id, {
11808
+ name: m.senderName || m.senderPubkey.slice(0, 8),
11809
+ snippet: text.replace(/\s+/g, " ").slice(0, 60)
11810
+ });
11811
+ if (cache2.size > RECENT_CACHE_MAX) {
11812
+ const firstKey = cache2.keys().next().value;
11813
+ if (firstKey)
11814
+ cache2.delete(firstKey);
11815
+ }
11816
+ }
11801
11817
  function decodeV1(b64) {
11802
11818
  try {
11803
11819
  return Buffer.from(b64, "base64").toString("utf-8");
@@ -11824,15 +11840,24 @@ function fmtTime(iso) {
11824
11840
  return iso;
11825
11841
  }
11826
11842
  }
11827
- async function printMessage(m, topicKey, json) {
11843
+ async function printMessage(m, topicKey, json, cache2) {
11828
11844
  const text = await decryptForRender(m, topicKey);
11829
11845
  if (json) {
11830
11846
  console.log(JSON.stringify({ ...m, message: text }));
11847
+ rememberRendered(cache2, m, text);
11831
11848
  return;
11832
11849
  }
11833
11850
  const v2Marker = (m.bodyVersion ?? 1) === 2 ? dim("\uD83D\uDD12 ") : "";
11834
- process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${v2Marker}${text}
11851
+ if (m.replyToId) {
11852
+ const parent = cache2.get(m.replyToId);
11853
+ const ref = parent ? `${parent.name}: "${parent.snippet}${parent.snippet.length === 60 ? "…" : ""}"` : `${m.replyToId.slice(0, 8)}…`;
11854
+ process.stdout.write(` ${dim("↳ in reply to " + ref)}
11835
11855
  `);
11856
+ }
11857
+ const idTag = dim(`#${m.id.slice(0, 8)}`);
11858
+ process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${idTag} ${v2Marker}${text}
11859
+ `);
11860
+ rememberRendered(cache2, m, text);
11836
11861
  }
11837
11862
  async function* readSseStream(reader) {
11838
11863
  const decoder = new TextDecoder;
@@ -11891,6 +11916,7 @@ async function runTopicTail(name, flags) {
11891
11916
  topicName: cleanName
11892
11917
  });
11893
11918
  const topicKey = keyResult.ok ? keyResult.topicKey ?? null : null;
11919
+ const snippetCache = new Map;
11894
11920
  let resealTimer = null;
11895
11921
  if (topicKey) {
11896
11922
  const reseal = async () => {
@@ -11943,7 +11969,7 @@ async function runTopicTail(name, flags) {
11943
11969
  render.section(`${clay("#" + cleanName)} on ${dim(meshSlug)} — backfill ${history.messages.length}, then live`);
11944
11970
  }
11945
11971
  for (const m of history.messages.slice().reverse()) {
11946
- await printMessage(m, topicKey, flags.json ?? false);
11972
+ await printMessage(m, topicKey, flags.json ?? false, snippetCache);
11947
11973
  }
11948
11974
  } catch (err) {
11949
11975
  render.warn(`backfill failed: ${err.message}`);
@@ -11982,7 +12008,7 @@ async function runTopicTail(name, flags) {
11982
12008
  if (ev.event === "message") {
11983
12009
  try {
11984
12010
  const m = JSON.parse(ev.data);
11985
- await printMessage(m, topicKey, flags.json ?? false);
12011
+ await printMessage(m, topicKey, flags.json ?? false, snippetCache);
11986
12012
  } catch {}
11987
12013
  }
11988
12014
  }
@@ -12000,6 +12026,7 @@ async function runTopicTail(name, flags) {
12000
12026
  }
12001
12027
  });
12002
12028
  }
12029
+ var RECENT_CACHE_MAX = 256;
12003
12030
  var init_topic_tail = __esm(() => {
12004
12031
  init_urls();
12005
12032
  init_with_rest_key();
@@ -12060,6 +12087,27 @@ async function runTopicPost(topicName, message, flags) {
12060
12087
  return EXIT.INTERNAL_ERROR;
12061
12088
  }
12062
12089
  }
12090
+ let replyToId;
12091
+ if (flags.replyTo) {
12092
+ if (flags.replyTo.length >= 16) {
12093
+ replyToId = flags.replyTo;
12094
+ } else if (flags.replyTo.length >= 6) {
12095
+ const recent = await request({
12096
+ path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=200`,
12097
+ method: "GET",
12098
+ token: secret
12099
+ });
12100
+ const hit = recent.messages?.find((r) => r.id.startsWith(flags.replyTo));
12101
+ if (!hit) {
12102
+ render.err(`--reply-to ${flags.replyTo}: no recent message id starts with that prefix`);
12103
+ return EXIT.INVALID_ARGS;
12104
+ }
12105
+ replyToId = hit.id;
12106
+ } else {
12107
+ render.err("--reply-to needs at least 6 characters of the message id");
12108
+ return EXIT.INVALID_ARGS;
12109
+ }
12110
+ }
12063
12111
  const result = await request({
12064
12112
  path: "/api/v1/messages",
12065
12113
  method: "POST",
@@ -12069,7 +12117,8 @@ async function runTopicPost(topicName, message, flags) {
12069
12117
  ciphertext,
12070
12118
  nonce,
12071
12119
  bodyVersion,
12072
- ...mentions.length > 0 ? { mentions } : {}
12120
+ ...mentions.length > 0 ? { mentions } : {},
12121
+ ...replyToId ? { replyToId } : {}
12073
12122
  }
12074
12123
  });
12075
12124
  if (flags.json) {
@@ -12077,7 +12126,8 @@ async function runTopicPost(topicName, message, flags) {
12077
12126
  return EXIT.SUCCESS;
12078
12127
  }
12079
12128
  const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
12080
- render.ok("posted", `${clay("#" + cleanName)} ${versionTag} ${dim(`(${result.notifications} mentions)`)}`);
12129
+ const replyTag = result.replyToId ? ` ${dim("" + result.replyToId.slice(0, 8))}` : "";
12130
+ render.ok("posted", `${clay("#" + cleanName)} ${versionTag}${replyTag} ${dim(`(${result.notifications} mentions)`)}`);
12081
12131
  return EXIT.SUCCESS;
12082
12132
  });
12083
12133
  }
@@ -12445,7 +12495,16 @@ async function startMcpServer() {
12445
12495
  You are "${myName}"${myRole ? ` (${myRole})` : ""} — a peer in the claudemesh network. Your groups: ${myGroups}. You are one of several Claude Code sessions connected to the same mesh. No orchestrator exists — peers are equals. Your identity comes from your name and group roles, not from a central authority.
12446
12496
 
12447
12497
  ## Responding to messages
12448
- When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Pause your current task, reply via send_message, then resume. Read from_name, mesh_slug, and priority from the channel attributes. Reply by setting \`to\` to the sender's from_name (display name). Stay in character per your system prompt. Do not ignore low-priority messages — acknowledge them briefly even if you defer action.
12498
+ When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Pause your current task, reply via send_message (or \`claudemesh topic post --reply-to <message_id>\` for topic threads), then resume. Stay in character per your system prompt. Do not ignore low-priority messages — acknowledge them briefly even if you defer action.
12499
+
12500
+ The channel attributes carry everything you need to reply — no extra lookups:
12501
+ - \`from_name\` — sender display name. Use as the \`to\` arg when replying to a DM.
12502
+ - \`from_pubkey\` / \`from_member_id\` — stable ids. Use \`from_member_id\` if the sender's display name might change.
12503
+ - \`mesh_slug\` — pass via \`--mesh\` if your default mesh differs.
12504
+ - \`priority\` — \`now\` / \`next\` / \`low\`.
12505
+ - \`message_id\` — id of THIS message. To thread a reply onto it in a topic, run \`claudemesh topic post <topic> "<text>" --reply-to <message_id>\`.
12506
+ - \`topic\` — set when the message arrived through a topic (vs DM). Reply in the same topic.
12507
+ - \`reply_to_id\` — set when the incoming message is itself a reply. Render thread context if you re-narrate.
12449
12508
 
12450
12509
  If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder you set for yourself — act on it immediately (no reply needed).
12451
12510
 
@@ -12805,20 +12864,27 @@ ${manifest.allowed_tools.map((t) => ` - ${t}`).join(`
12805
12864
  const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
12806
12865
  const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
12807
12866
  const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
12867
+ const fromMemberPubkey = msg.senderMemberPubkey ?? fromPubkey;
12808
12868
  try {
12809
12869
  await server.notification({
12810
12870
  method: "notifications/claude/channel",
12811
12871
  params: {
12812
12872
  content,
12813
12873
  meta: {
12814
- from_id: fromPubkey,
12874
+ from_id: fromMemberPubkey,
12875
+ from_pubkey: fromMemberPubkey,
12876
+ from_session_pubkey: fromPubkey,
12815
12877
  from_name: fromName,
12878
+ ...msg.senderMemberId ? { from_member_id: msg.senderMemberId } : {},
12816
12879
  mesh_slug: client.meshSlug,
12817
12880
  mesh_id: client.meshId,
12818
12881
  priority: msg.priority,
12819
12882
  sent_at: msg.createdAt,
12820
12883
  delivered_at: msg.receivedAt,
12821
12884
  kind: msg.kind,
12885
+ message_id: msg.messageId,
12886
+ ...msg.topic ? { topic: msg.topic } : {},
12887
+ ...msg.replyToId ? { reply_to_id: msg.replyToId } : {},
12822
12888
  ...msg.subtype ? { subtype: msg.subtype } : {}
12823
12889
  }
12824
12890
  }
@@ -13772,7 +13838,7 @@ Topic (conversation scope, v0.2.0)
13772
13838
  claudemesh topic history <t> fetch message history [--limit --before]
13773
13839
  claudemesh topic read <topic> mark all as read
13774
13840
  claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
13775
- claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2)
13841
+ claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2) [--reply-to <id>]
13776
13842
  claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
13777
13843
  claudemesh member list mesh roster with online state [--online]
13778
13844
  claudemesh notification list recent @-mentions of you [--since <ISO>]
@@ -14567,7 +14633,8 @@ async function main() {
14567
14633
  const postFlags = {
14568
14634
  mesh: flags.mesh,
14569
14635
  json: !!flags.json,
14570
- plaintext: !!flags.plaintext
14636
+ plaintext: !!flags.plaintext,
14637
+ replyTo: flags["reply-to"] || flags.replyTo
14571
14638
  };
14572
14639
  const message = positionals.slice(2).join(" ");
14573
14640
  const { runTopicPost: runTopicPost2 } = await Promise.resolve().then(() => (init_topic_post(), exports_topic_post));
@@ -14660,4 +14727,4 @@ main().catch((err) => {
14660
14727
  process.exit(EXIT.INTERNAL_ERROR);
14661
14728
  });
14662
14729
 
14663
- //# debugId=25484051F3F24E9464756E2164756E21
14730
+ //# debugId=25CA2A7E4B8DBD3564756E2164756E21