claudemesh-cli 1.8.0 → 1.9.3

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.3", 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,
@@ -7078,6 +7083,61 @@ var init_recall = __esm(() => {
7078
7083
  init_exit_codes();
7079
7084
  });
7080
7085
 
7086
+ // src/cli/validators.ts
7087
+ function validateMessageId(input) {
7088
+ if (!input) {
7089
+ return {
7090
+ ok: false,
7091
+ code: "missing",
7092
+ reason: "message id is required",
7093
+ expected: "32-char base62 id, or ≥8-char prefix"
7094
+ };
7095
+ }
7096
+ if (input.length < 8) {
7097
+ return {
7098
+ ok: false,
7099
+ code: "too_short",
7100
+ reason: `id is ${input.length} chars, needs ≥8`,
7101
+ expected: "8+ chars (paste from a previous send/post output)"
7102
+ };
7103
+ }
7104
+ if (input.length > 32) {
7105
+ return {
7106
+ ok: false,
7107
+ code: "too_long",
7108
+ reason: `id is ${input.length} chars, max 32`,
7109
+ expected: "trim trailing characters"
7110
+ };
7111
+ }
7112
+ if (!BASE62_RE.test(input)) {
7113
+ return {
7114
+ ok: false,
7115
+ code: "bad_charset",
7116
+ reason: "id contains characters outside [A-Za-z0-9]",
7117
+ expected: "base62 only"
7118
+ };
7119
+ }
7120
+ return { ok: true, value: { value: input, isPrefix: input.length < 32 } };
7121
+ }
7122
+ function renderValidationError(args, write = (s) => process.stderr.write(s)) {
7123
+ write(` \x1B[31m✘\x1B[0m ${args.verb} ${args.input}
7124
+ `);
7125
+ write(` ${args.result.reason}.
7126
+ `);
7127
+ if (args.result.expected) {
7128
+ write(` expected: ${args.result.expected}
7129
+ `);
7130
+ }
7131
+ if (args.nearest) {
7132
+ write(` did you mean: \x1B[36m${args.nearest}\x1B[0m
7133
+ `);
7134
+ }
7135
+ }
7136
+ var BASE62_RE;
7137
+ var init_validators = __esm(() => {
7138
+ BASE62_RE = /^[A-Za-z0-9]+$/;
7139
+ });
7140
+
7081
7141
  // src/commands/broker-actions.ts
7082
7142
  var exports_broker_actions = {};
7083
7143
  __export(exports_broker_actions, {
@@ -7241,17 +7301,44 @@ async function runForget(id, opts) {
7241
7301
  return EXIT.SUCCESS;
7242
7302
  }
7243
7303
  async function runMsgStatus(id, opts) {
7244
- if (!id) {
7245
- render.err("Usage: claudemesh msg-status <message-id>");
7304
+ const v = validateMessageId(id);
7305
+ if (!v.ok) {
7306
+ if (opts.json) {
7307
+ console.log(JSON.stringify({
7308
+ ok: false,
7309
+ error: "invalid_argument",
7310
+ field: "messageId",
7311
+ code: v.code,
7312
+ reason: v.reason,
7313
+ expected: v.expected
7314
+ }));
7315
+ } else {
7316
+ renderValidationError({
7317
+ verb: "msg-status",
7318
+ input: id ?? "(missing)",
7319
+ result: v
7320
+ });
7321
+ }
7246
7322
  return EXIT.INVALID_ARGS;
7247
7323
  }
7324
+ const lookupId = v.value.value;
7248
7325
  return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
7249
- const result = await client.messageStatus(id);
7326
+ const result = await client.messageStatus(lookupId);
7250
7327
  if (!result) {
7251
- if (opts.json)
7252
- console.log(JSON.stringify({ id, found: false }));
7253
- else
7254
- render.err(`Message ${id} not found or timed out.`);
7328
+ if (opts.json) {
7329
+ console.log(JSON.stringify({
7330
+ ok: false,
7331
+ error: "not_found",
7332
+ id: lookupId,
7333
+ isPrefix: v.value.isPrefix
7334
+ }));
7335
+ } else {
7336
+ const hint = v.value.isPrefix ? ` no message id starts with ${dim('"' + lookupId + '"')} in this mesh.
7337
+ try: claudemesh msg-status <full-32-char-id>` : ` message ${dim(lookupId.slice(0, 12) + "…")} not in queue (already drained, expired, or never sent in this mesh).`;
7338
+ render.err(`message not found`);
7339
+ process.stderr.write(hint + `
7340
+ `);
7341
+ }
7255
7342
  return EXIT.NOT_FOUND;
7256
7343
  }
7257
7344
  if (opts.json) {
@@ -7383,6 +7470,7 @@ var init_broker_actions = __esm(() => {
7383
7470
  init_render();
7384
7471
  init_styles();
7385
7472
  init_exit_codes();
7473
+ init_validators();
7386
7474
  });
7387
7475
 
7388
7476
  // src/commands/remind.ts
@@ -11798,6 +11886,17 @@ var exports_topic_tail = {};
11798
11886
  __export(exports_topic_tail, {
11799
11887
  runTopicTail: () => runTopicTail
11800
11888
  });
11889
+ function rememberRendered(cache2, m, text) {
11890
+ cache2.set(m.id, {
11891
+ name: m.senderName || m.senderPubkey.slice(0, 8),
11892
+ snippet: text.replace(/\s+/g, " ").slice(0, 60)
11893
+ });
11894
+ if (cache2.size > RECENT_CACHE_MAX) {
11895
+ const firstKey = cache2.keys().next().value;
11896
+ if (firstKey)
11897
+ cache2.delete(firstKey);
11898
+ }
11899
+ }
11801
11900
  function decodeV1(b64) {
11802
11901
  try {
11803
11902
  return Buffer.from(b64, "base64").toString("utf-8");
@@ -11824,15 +11923,24 @@ function fmtTime(iso) {
11824
11923
  return iso;
11825
11924
  }
11826
11925
  }
11827
- async function printMessage(m, topicKey, json) {
11926
+ async function printMessage(m, topicKey, json, cache2) {
11828
11927
  const text = await decryptForRender(m, topicKey);
11829
11928
  if (json) {
11830
11929
  console.log(JSON.stringify({ ...m, message: text }));
11930
+ rememberRendered(cache2, m, text);
11831
11931
  return;
11832
11932
  }
11833
11933
  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}
11934
+ if (m.replyToId) {
11935
+ const parent = cache2.get(m.replyToId);
11936
+ const ref = parent ? `${parent.name}: "${parent.snippet}${parent.snippet.length === 60 ? "…" : ""}"` : `${m.replyToId.slice(0, 8)}…`;
11937
+ process.stdout.write(` ${dim("↳ in reply to " + ref)}
11938
+ `);
11939
+ }
11940
+ const idTag = dim(`#${m.id.slice(0, 8)}`);
11941
+ process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${idTag} ${v2Marker}${text}
11835
11942
  `);
11943
+ rememberRendered(cache2, m, text);
11836
11944
  }
11837
11945
  async function* readSseStream(reader) {
11838
11946
  const decoder = new TextDecoder;
@@ -11891,6 +11999,7 @@ async function runTopicTail(name, flags) {
11891
11999
  topicName: cleanName
11892
12000
  });
11893
12001
  const topicKey = keyResult.ok ? keyResult.topicKey ?? null : null;
12002
+ const snippetCache = new Map;
11894
12003
  let resealTimer = null;
11895
12004
  if (topicKey) {
11896
12005
  const reseal = async () => {
@@ -11943,7 +12052,7 @@ async function runTopicTail(name, flags) {
11943
12052
  render.section(`${clay("#" + cleanName)} on ${dim(meshSlug)} — backfill ${history.messages.length}, then live`);
11944
12053
  }
11945
12054
  for (const m of history.messages.slice().reverse()) {
11946
- await printMessage(m, topicKey, flags.json ?? false);
12055
+ await printMessage(m, topicKey, flags.json ?? false, snippetCache);
11947
12056
  }
11948
12057
  } catch (err) {
11949
12058
  render.warn(`backfill failed: ${err.message}`);
@@ -11982,7 +12091,7 @@ async function runTopicTail(name, flags) {
11982
12091
  if (ev.event === "message") {
11983
12092
  try {
11984
12093
  const m = JSON.parse(ev.data);
11985
- await printMessage(m, topicKey, flags.json ?? false);
12094
+ await printMessage(m, topicKey, flags.json ?? false, snippetCache);
11986
12095
  } catch {}
11987
12096
  }
11988
12097
  }
@@ -12000,6 +12109,7 @@ async function runTopicTail(name, flags) {
12000
12109
  }
12001
12110
  });
12002
12111
  }
12112
+ var RECENT_CACHE_MAX = 256;
12003
12113
  var init_topic_tail = __esm(() => {
12004
12114
  init_urls();
12005
12115
  init_with_rest_key();
@@ -12060,6 +12170,27 @@ async function runTopicPost(topicName, message, flags) {
12060
12170
  return EXIT.INTERNAL_ERROR;
12061
12171
  }
12062
12172
  }
12173
+ let replyToId;
12174
+ if (flags.replyTo) {
12175
+ if (flags.replyTo.length >= 16) {
12176
+ replyToId = flags.replyTo;
12177
+ } else if (flags.replyTo.length >= 6) {
12178
+ const recent = await request({
12179
+ path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=200`,
12180
+ method: "GET",
12181
+ token: secret
12182
+ });
12183
+ const hit = recent.messages?.find((r) => r.id.startsWith(flags.replyTo));
12184
+ if (!hit) {
12185
+ render.err(`--reply-to ${flags.replyTo}: no recent message id starts with that prefix`);
12186
+ return EXIT.INVALID_ARGS;
12187
+ }
12188
+ replyToId = hit.id;
12189
+ } else {
12190
+ render.err("--reply-to needs at least 6 characters of the message id");
12191
+ return EXIT.INVALID_ARGS;
12192
+ }
12193
+ }
12063
12194
  const result = await request({
12064
12195
  path: "/api/v1/messages",
12065
12196
  method: "POST",
@@ -12069,7 +12200,8 @@ async function runTopicPost(topicName, message, flags) {
12069
12200
  ciphertext,
12070
12201
  nonce,
12071
12202
  bodyVersion,
12072
- ...mentions.length > 0 ? { mentions } : {}
12203
+ ...mentions.length > 0 ? { mentions } : {},
12204
+ ...replyToId ? { replyToId } : {}
12073
12205
  }
12074
12206
  });
12075
12207
  if (flags.json) {
@@ -12077,7 +12209,8 @@ async function runTopicPost(topicName, message, flags) {
12077
12209
  return EXIT.SUCCESS;
12078
12210
  }
12079
12211
  const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
12080
- render.ok("posted", `${clay("#" + cleanName)} ${versionTag} ${dim(`(${result.notifications} mentions)`)}`);
12212
+ const replyTag = result.replyToId ? ` ${dim("" + result.replyToId.slice(0, 8))}` : "";
12213
+ render.ok("posted", `${clay("#" + cleanName)} ${versionTag}${replyTag} ${dim(`(${result.notifications} mentions)`)}`);
12081
12214
  return EXIT.SUCCESS;
12082
12215
  });
12083
12216
  }
@@ -12445,7 +12578,16 @@ async function startMcpServer() {
12445
12578
  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
12579
 
12447
12580
  ## 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.
12581
+ 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.
12582
+
12583
+ The channel attributes carry everything you need to reply — no extra lookups:
12584
+ - \`from_name\` — sender display name. Use as the \`to\` arg when replying to a DM.
12585
+ - \`from_pubkey\` / \`from_member_id\` — stable ids. Use \`from_member_id\` if the sender's display name might change.
12586
+ - \`mesh_slug\` — pass via \`--mesh\` if your default mesh differs.
12587
+ - \`priority\` — \`now\` / \`next\` / \`low\`.
12588
+ - \`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>\`.
12589
+ - \`topic\` — set when the message arrived through a topic (vs DM). Reply in the same topic.
12590
+ - \`reply_to_id\` — set when the incoming message is itself a reply. Render thread context if you re-narrate.
12449
12591
 
12450
12592
  If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder you set for yourself — act on it immediately (no reply needed).
12451
12593
 
@@ -12805,20 +12947,27 @@ ${manifest.allowed_tools.map((t) => ` - ${t}`).join(`
12805
12947
  const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
12806
12948
  const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
12807
12949
  const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
12950
+ const fromMemberPubkey = msg.senderMemberPubkey ?? fromPubkey;
12808
12951
  try {
12809
12952
  await server.notification({
12810
12953
  method: "notifications/claude/channel",
12811
12954
  params: {
12812
12955
  content,
12813
12956
  meta: {
12814
- from_id: fromPubkey,
12957
+ from_id: fromMemberPubkey,
12958
+ from_pubkey: fromMemberPubkey,
12959
+ from_session_pubkey: fromPubkey,
12815
12960
  from_name: fromName,
12961
+ ...msg.senderMemberId ? { from_member_id: msg.senderMemberId } : {},
12816
12962
  mesh_slug: client.meshSlug,
12817
12963
  mesh_id: client.meshId,
12818
12964
  priority: msg.priority,
12819
12965
  sent_at: msg.createdAt,
12820
12966
  delivered_at: msg.receivedAt,
12821
12967
  kind: msg.kind,
12968
+ message_id: msg.messageId,
12969
+ ...msg.topic ? { topic: msg.topic } : {},
12970
+ ...msg.replyToId ? { reply_to_id: msg.replyToId } : {},
12822
12971
  ...msg.subtype ? { subtype: msg.subtype } : {}
12823
12972
  }
12824
12973
  }
@@ -13682,6 +13831,7 @@ async function gate(ctx, opts) {
13682
13831
  }
13683
13832
 
13684
13833
  // src/entrypoints/cli.ts
13834
+ init_styles();
13685
13835
  installSignalHandlers();
13686
13836
  installErrorHandlers();
13687
13837
  var { command, positionals, flags } = parseArgv(process.argv);
@@ -13772,7 +13922,7 @@ Topic (conversation scope, v0.2.0)
13772
13922
  claudemesh topic history <t> fetch message history [--limit --before]
13773
13923
  claudemesh topic read <topic> mark all as read
13774
13924
  claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
13775
- claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2)
13925
+ claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2) [--reply-to <id>]
13776
13926
  claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
13777
13927
  claudemesh member list mesh roster with online state [--online]
13778
13928
  claudemesh notification list recent @-mentions of you [--since <ISO>]
@@ -13843,9 +13993,44 @@ Flags
13843
13993
  -y, --yes skip confirmations (= --approval-mode yolo)
13844
13994
  -q, --quiet suppress non-essential output
13845
13995
  `;
13996
+ function colorizeHelp(raw) {
13997
+ const lines = raw.split(`
13998
+ `);
13999
+ const SECTION_HEADER_RE = /^([A-Z][A-Za-z0-9 /+-]*?)(\s*\(.*\))?$/;
14000
+ const VERB_ROW_RE = /^(\s{2})(claudemesh[^\s]*(?:\s+[^\s]+)*?)(\s{2,})(.*)$/;
14001
+ const ALIAS_RE = /(\(alias[^)]*\))/g;
14002
+ const out = [];
14003
+ for (const line of lines) {
14004
+ if (line.startsWith("claudemesh —")) {
14005
+ out.push(orange(line));
14006
+ continue;
14007
+ }
14008
+ if (line.trim() === "") {
14009
+ out.push(line);
14010
+ continue;
14011
+ }
14012
+ if (!line.startsWith(" ") && SECTION_HEADER_RE.test(line)) {
14013
+ const m = line.match(SECTION_HEADER_RE);
14014
+ const head = bold(clay(m[1]));
14015
+ const meta = m[2] ? dim(m[2]) : "";
14016
+ out.push(head + meta);
14017
+ continue;
14018
+ }
14019
+ const verbMatch = line.match(VERB_ROW_RE);
14020
+ if (verbMatch) {
14021
+ const [, indent, syntax, gap, rest] = verbMatch;
14022
+ const dimmedRest = rest.replace(ALIAS_RE, (m) => dim(m));
14023
+ out.push(`${indent}${cyan(syntax)}${gap}${dimmedRest}`);
14024
+ continue;
14025
+ }
14026
+ out.push(line);
14027
+ }
14028
+ return out.join(`
14029
+ `);
14030
+ }
13846
14031
  async function main() {
13847
14032
  if (flags.help || flags.h) {
13848
- console.log(HELP);
14033
+ console.log(colorizeHelp(HELP));
13849
14034
  process.exit(EXIT.SUCCESS);
13850
14035
  }
13851
14036
  if (flags.version || flags.V) {
@@ -14567,7 +14752,8 @@ async function main() {
14567
14752
  const postFlags = {
14568
14753
  mesh: flags.mesh,
14569
14754
  json: !!flags.json,
14570
- plaintext: !!flags.plaintext
14755
+ plaintext: !!flags.plaintext,
14756
+ replyTo: flags["reply-to"] || flags.replyTo
14571
14757
  };
14572
14758
  const message = positionals.slice(2).join(" ");
14573
14759
  const { runTopicPost: runTopicPost2 } = await Promise.resolve().then(() => (init_topic_post(), exports_topic_post));
@@ -14660,4 +14846,4 @@ main().catch((err) => {
14660
14846
  process.exit(EXIT.INTERNAL_ERROR);
14661
14847
  });
14662
14848
 
14663
- //# debugId=25484051F3F24E9464756E2164756E21
14849
+ //# debugId=AEE2B186929FB60F64756E2164756E21