claudemesh-cli 1.6.1 → 1.7.0

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.6.0:** topics (channel pub/sub), API keys for human/REST clients, and bridge peers that forward a topic between two meshes. New verbs: `claudemesh topic`, `claudemesh apikey`, `claudemesh bridge`. A REST surface at `https://claudemesh.com/api/v1/*` (messages, topics, peers, history) accepts `Authorization: Bearer cm_...` keys, so any HTTPS client can participate without WebSocket + ed25519 plumbing. **Note**: REST lives on the web host (`claudemesh.com`), not the broker host (`ic.claudemesh.com`) the broker only speaks WebSocket.
5
+ > **What's 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.
6
+ >
7
+ > **What was new in 1.6.0:** topics (channel pub/sub), API keys for human/REST clients, and bridge peers that forward a topic between two meshes. New verbs: `claudemesh topic`, `claudemesh apikey`, `claudemesh bridge`. A REST surface at `https://claudemesh.com/api/v1/*` (messages, topics, peers, history) accepts `Authorization: Bearer cm_...` keys, so any HTTPS client can participate without WebSocket + ed25519 plumbing. **Note**: REST lives on the web host (`claudemesh.com`), not the broker host (`ic.claudemesh.com`) — the broker only speaks WebSocket.
6
8
  >
7
9
  > **Migration note (1.5.0):** the previous 79 MCP tools (`send_message`, `list_peers`, `remember`, …) are removed. Use the matching CLI verbs (`claudemesh send`, `claudemesh peers`, `claudemesh remember`). Run `claudemesh install` and the bundled skill teaches Claude the full surface.
8
10
 
@@ -43,6 +45,9 @@ USAGE
43
45
  claudemesh profile view or edit your profile
44
46
 
45
47
  claudemesh topic ... create, list, join, send to topics
48
+ claudemesh topic tail <t> live SSE tail of a topic
49
+ claudemesh member list mesh roster with online state
50
+ claudemesh notification list recent @-mentions of you
46
51
  claudemesh apikey ... issue, list, revoke API keys (REST clients)
47
52
  claudemesh bridge ... forward a topic between two meshes
48
53
 
@@ -88,7 +88,7 @@ __export(exports_urls, {
88
88
  VERSION: () => VERSION,
89
89
  URLS: () => URLS
90
90
  });
91
- var URLS, VERSION = "1.6.1", env;
91
+ var URLS, VERSION = "1.7.0", env;
92
92
  var init_urls = __esm(() => {
93
93
  URLS = {
94
94
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -11648,6 +11648,304 @@ var init_topic = __esm(() => {
11648
11648
  init_exit_codes();
11649
11649
  });
11650
11650
 
11651
+ // src/services/api/with-rest-key.ts
11652
+ async function withRestKey(opts, fn) {
11653
+ return withMesh({ meshSlug: opts.meshSlug ?? null }, async (client, mesh) => {
11654
+ const result = await client.apiKeyCreate({
11655
+ label: `cli-${opts.purpose ?? "rest"}-${process.pid}`,
11656
+ capabilities: opts.capabilities ?? ["read"],
11657
+ topicScopes: opts.topicScopes ?? undefined,
11658
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
11659
+ });
11660
+ if (!result || !result.secret) {
11661
+ throw new Error("apikey mint failed — broker did not return a secret");
11662
+ }
11663
+ try {
11664
+ return await fn({
11665
+ secret: result.secret,
11666
+ meshId: mesh.meshId,
11667
+ meshSlug: mesh.slug,
11668
+ client,
11669
+ mesh
11670
+ });
11671
+ } finally {
11672
+ try {
11673
+ await client.apiKeyRevoke(result.id);
11674
+ } catch {}
11675
+ }
11676
+ });
11677
+ }
11678
+ var init_with_rest_key = __esm(() => {
11679
+ init_connect();
11680
+ });
11681
+
11682
+ // src/commands/topic-tail.ts
11683
+ var exports_topic_tail = {};
11684
+ __export(exports_topic_tail, {
11685
+ runTopicTail: () => runTopicTail
11686
+ });
11687
+ function decodeCiphertext(b64) {
11688
+ try {
11689
+ return Buffer.from(b64, "base64").toString("utf-8");
11690
+ } catch {
11691
+ return "[decode failed]";
11692
+ }
11693
+ }
11694
+ function fmtTime(iso) {
11695
+ try {
11696
+ return new Date(iso).toLocaleTimeString([], {
11697
+ hour: "2-digit",
11698
+ minute: "2-digit",
11699
+ second: "2-digit"
11700
+ });
11701
+ } catch {
11702
+ return iso;
11703
+ }
11704
+ }
11705
+ function printMessage(m, json) {
11706
+ const text = decodeCiphertext(m.ciphertext);
11707
+ if (json) {
11708
+ console.log(JSON.stringify({ ...m, message: text }));
11709
+ return;
11710
+ }
11711
+ process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${text}
11712
+ `);
11713
+ }
11714
+ async function* readSseStream(reader) {
11715
+ const decoder = new TextDecoder;
11716
+ let buffer = "";
11717
+ while (true) {
11718
+ const { value, done } = await reader.read();
11719
+ if (done)
11720
+ break;
11721
+ buffer += decoder.decode(value, { stream: true });
11722
+ let idx;
11723
+ while ((idx = buffer.indexOf(`
11724
+
11725
+ `)) !== -1) {
11726
+ const block = buffer.slice(0, idx);
11727
+ buffer = buffer.slice(idx + 2);
11728
+ const ev = { event: "message", data: "" };
11729
+ const dataLines = [];
11730
+ for (const line of block.split(`
11731
+ `)) {
11732
+ if (!line || line.startsWith(":"))
11733
+ continue;
11734
+ const colon = line.indexOf(":");
11735
+ if (colon < 0)
11736
+ continue;
11737
+ const field = line.slice(0, colon);
11738
+ const val = line.slice(colon + 1).replace(/^ /, "");
11739
+ if (field === "event")
11740
+ ev.event = val;
11741
+ else if (field === "id")
11742
+ ev.id = val;
11743
+ else if (field === "data")
11744
+ dataLines.push(val);
11745
+ }
11746
+ ev.data = dataLines.join(`
11747
+ `);
11748
+ yield ev;
11749
+ }
11750
+ }
11751
+ }
11752
+ async function runTopicTail(name, flags) {
11753
+ if (!name) {
11754
+ render.err("Usage: claudemesh topic tail <topic> [--limit N]");
11755
+ return EXIT.INVALID_ARGS;
11756
+ }
11757
+ const cleanName = name.replace(/^#/, "");
11758
+ const limit = flags.limit ? Number(flags.limit) : 20;
11759
+ return withRestKey({
11760
+ meshSlug: flags.mesh ?? null,
11761
+ purpose: `tail-${cleanName}`,
11762
+ capabilities: ["read"],
11763
+ topicScopes: [cleanName]
11764
+ }, async ({ secret, meshSlug }) => {
11765
+ if (!flags.forwardOnly && limit > 0) {
11766
+ try {
11767
+ const history = await request({
11768
+ path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=${limit}`,
11769
+ token: secret
11770
+ });
11771
+ if (!flags.json) {
11772
+ render.section(`${clay("#" + cleanName)} on ${dim(meshSlug)} — backfill ${history.messages.length}, then live`);
11773
+ }
11774
+ for (const m of history.messages.slice().reverse()) {
11775
+ printMessage(m, flags.json ?? false);
11776
+ }
11777
+ } catch (err) {
11778
+ render.warn(`backfill failed: ${err.message}`);
11779
+ }
11780
+ }
11781
+ const url = `${URLS.API_BASE}/api/v1/topics/${encodeURIComponent(cleanName)}/stream`;
11782
+ const ctl = new AbortController;
11783
+ const onSig = () => ctl.abort();
11784
+ process.once("SIGINT", onSig);
11785
+ process.once("SIGTERM", onSig);
11786
+ try {
11787
+ const res = await fetch(url, {
11788
+ headers: { Authorization: `Bearer ${secret}` },
11789
+ signal: ctl.signal
11790
+ });
11791
+ if (!res.ok || !res.body) {
11792
+ render.err(`stream open failed: ${res.status}`);
11793
+ return EXIT.INTERNAL_ERROR;
11794
+ }
11795
+ if (!flags.json) {
11796
+ render.info(dim("tailing — Ctrl-C to exit"));
11797
+ }
11798
+ const reader = res.body.getReader();
11799
+ for await (const ev of readSseStream(reader)) {
11800
+ if (ev.event === "ready" || ev.event === "heartbeat")
11801
+ continue;
11802
+ if (ev.event === "error") {
11803
+ try {
11804
+ const parsed = JSON.parse(ev.data);
11805
+ render.err(`stream error: ${parsed.error ?? "unknown"}`);
11806
+ } catch {
11807
+ render.err("stream error");
11808
+ }
11809
+ continue;
11810
+ }
11811
+ if (ev.event === "message") {
11812
+ try {
11813
+ const m = JSON.parse(ev.data);
11814
+ printMessage(m, flags.json ?? false);
11815
+ } catch {}
11816
+ }
11817
+ }
11818
+ return EXIT.SUCCESS;
11819
+ } catch (err) {
11820
+ if (ctl.signal.aborted)
11821
+ return EXIT.SUCCESS;
11822
+ render.err(`tail aborted: ${err.message}`);
11823
+ return EXIT.INTERNAL_ERROR;
11824
+ } finally {
11825
+ process.removeListener("SIGINT", onSig);
11826
+ process.removeListener("SIGTERM", onSig);
11827
+ }
11828
+ });
11829
+ }
11830
+ var init_topic_tail = __esm(() => {
11831
+ init_urls();
11832
+ init_with_rest_key();
11833
+ init_client();
11834
+ init_render();
11835
+ init_styles();
11836
+ init_exit_codes();
11837
+ });
11838
+
11839
+ // src/commands/notification.ts
11840
+ var exports_notification = {};
11841
+ __export(exports_notification, {
11842
+ runNotificationList: () => runNotificationList
11843
+ });
11844
+ function decodeCiphertext2(b64) {
11845
+ try {
11846
+ return Buffer.from(b64, "base64").toString("utf-8");
11847
+ } catch {
11848
+ return "[decode failed]";
11849
+ }
11850
+ }
11851
+ function fmtRelative(iso) {
11852
+ const ms = Date.now() - new Date(iso).getTime();
11853
+ if (ms < 60000)
11854
+ return "now";
11855
+ if (ms < 3600000)
11856
+ return `${Math.floor(ms / 60000)}m`;
11857
+ if (ms < 86400000)
11858
+ return `${Math.floor(ms / 3600000)}h`;
11859
+ return `${Math.floor(ms / 86400000)}d`;
11860
+ }
11861
+ async function runNotificationList(flags) {
11862
+ return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "notifications" }, async ({ secret }) => {
11863
+ const qs = flags.since ? `?since=${encodeURIComponent(flags.since)}` : "";
11864
+ const result = await request({
11865
+ path: `/api/v1/notifications${qs}`,
11866
+ token: secret
11867
+ });
11868
+ if (flags.json) {
11869
+ const decoded = result.notifications.map((n) => ({
11870
+ ...n,
11871
+ message: decodeCiphertext2(n.ciphertext)
11872
+ }));
11873
+ console.log(JSON.stringify({ ...result, notifications: decoded }, null, 2));
11874
+ return EXIT.SUCCESS;
11875
+ }
11876
+ if (result.notifications.length === 0) {
11877
+ render.info(dim(`no mentions of @${result.mentionedAs} since ${result.since}.`));
11878
+ return EXIT.SUCCESS;
11879
+ }
11880
+ render.section(`mentions of @${bold(result.mentionedAs)} (${result.notifications.length})`);
11881
+ for (const n of result.notifications) {
11882
+ const when = fmtRelative(n.createdAt);
11883
+ const msg = decodeCiphertext2(n.ciphertext).replace(/\s+/g, " ").trim();
11884
+ const snippet = msg.length > 100 ? msg.slice(0, 97) + "…" : msg;
11885
+ process.stdout.write(` ${clay("#" + n.topicName)} ${dim(when)} ${bold(n.senderName)}
11886
+ `);
11887
+ process.stdout.write(` ${snippet}
11888
+ `);
11889
+ }
11890
+ return EXIT.SUCCESS;
11891
+ });
11892
+ }
11893
+ var init_notification = __esm(() => {
11894
+ init_with_rest_key();
11895
+ init_client();
11896
+ init_render();
11897
+ init_styles();
11898
+ init_exit_codes();
11899
+ });
11900
+
11901
+ // src/commands/member.ts
11902
+ var exports_member = {};
11903
+ __export(exports_member, {
11904
+ runMemberList: () => runMemberList
11905
+ });
11906
+ function statusGlyph(m) {
11907
+ if (!m.online)
11908
+ return dim("○");
11909
+ if (m.status === "dnd")
11910
+ return red("●");
11911
+ if (m.status === "working")
11912
+ return yellow("●");
11913
+ return green("●");
11914
+ }
11915
+ async function runMemberList(flags) {
11916
+ return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "members" }, async ({ secret, meshSlug }) => {
11917
+ const result = await request({
11918
+ path: "/api/v1/members",
11919
+ token: secret
11920
+ });
11921
+ const filtered = flags.online ? result.members.filter((m) => m.online) : result.members;
11922
+ if (flags.json) {
11923
+ console.log(JSON.stringify({ members: filtered }, null, 2));
11924
+ return EXIT.SUCCESS;
11925
+ }
11926
+ if (filtered.length === 0) {
11927
+ render.info(dim(flags.online ? `no online members in ${meshSlug}.` : `no members in ${meshSlug}.`));
11928
+ return EXIT.SUCCESS;
11929
+ }
11930
+ const onlineCount = result.members.filter((m) => m.online).length;
11931
+ render.section(`${clay(meshSlug)} members (${onlineCount}/${result.members.length} online)`);
11932
+ for (const m of filtered) {
11933
+ const tag = m.isHuman ? dim("human") : dim("bot");
11934
+ const summary = m.summary ? ` — ${dim(m.summary)}` : "";
11935
+ process.stdout.write(` ${statusGlyph(m)} ${bold(m.displayName)} ${tag} ${dim(m.role)} ${dim(m.pubkey.slice(0, 8))}${summary}
11936
+ `);
11937
+ }
11938
+ return EXIT.SUCCESS;
11939
+ });
11940
+ }
11941
+ var init_member = __esm(() => {
11942
+ init_with_rest_key();
11943
+ init_client();
11944
+ init_render();
11945
+ init_styles();
11946
+ init_exit_codes();
11947
+ });
11948
+
11651
11949
  // src/mcp/tools/definitions.ts
11652
11950
  var TOOLS;
11653
11951
  var init_definitions = __esm(() => {
@@ -13219,7 +13517,10 @@ Topic (conversation scope, v0.2.0)
13219
13517
  claudemesh topic members <t> list topic subscribers
13220
13518
  claudemesh topic history <t> fetch message history [--limit --before]
13221
13519
  claudemesh topic read <topic> mark all as read
13520
+ claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
13222
13521
  claudemesh send "#topic" "msg" send to a topic
13522
+ claudemesh member list mesh roster with online state [--online]
13523
+ claudemesh notification list recent @-mentions of you [--since <ISO>]
13223
13524
 
13224
13525
  Schedule (resource form)
13225
13526
  claudemesh schedule msg <m> one-shot or recurring (alias: remind)
@@ -13998,8 +14299,51 @@ async function main() {
13998
14299
  } else if (sub === "read") {
13999
14300
  const { runTopicMarkRead: runTopicMarkRead2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
14000
14301
  process.exit(await runTopicMarkRead2(arg, f));
14302
+ } else if (sub === "tail") {
14303
+ const tailFlags = {
14304
+ mesh: flags.mesh,
14305
+ json: !!flags.json,
14306
+ limit: flags.limit,
14307
+ forwardOnly: !!flags["forward-only"]
14308
+ };
14309
+ const { runTopicTail: runTopicTail2 } = await Promise.resolve().then(() => (init_topic_tail(), exports_topic_tail));
14310
+ process.exit(await runTopicTail2(arg, tailFlags));
14311
+ } else {
14312
+ console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail>");
14313
+ process.exit(EXIT.INVALID_ARGS);
14314
+ }
14315
+ break;
14316
+ }
14317
+ case "notification":
14318
+ case "notifications": {
14319
+ const sub = positionals[0] ?? "list";
14320
+ const f = {
14321
+ mesh: flags.mesh,
14322
+ json: !!flags.json,
14323
+ since: flags.since
14324
+ };
14325
+ if (sub === "list") {
14326
+ const { runNotificationList: runNotificationList2 } = await Promise.resolve().then(() => (init_notification(), exports_notification));
14327
+ process.exit(await runNotificationList2(f));
14328
+ } else {
14329
+ console.error("Usage: claudemesh notification list [--since <ISO>]");
14330
+ process.exit(EXIT.INVALID_ARGS);
14331
+ }
14332
+ break;
14333
+ }
14334
+ case "member":
14335
+ case "members": {
14336
+ const sub = positionals[0] ?? "list";
14337
+ const f = {
14338
+ mesh: flags.mesh,
14339
+ json: !!flags.json,
14340
+ online: !!flags.online
14341
+ };
14342
+ if (sub === "list") {
14343
+ const { runMemberList: runMemberList2 } = await Promise.resolve().then(() => (init_member(), exports_member));
14344
+ process.exit(await runMemberList2(f));
14001
14345
  } else {
14002
- console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read>");
14346
+ console.error("Usage: claudemesh member list [--online]");
14003
14347
  process.exit(EXIT.INVALID_ARGS);
14004
14348
  }
14005
14349
  break;
@@ -14052,4 +14396,4 @@ main().catch((err) => {
14052
14396
  process.exit(EXIT.INTERNAL_ERROR);
14053
14397
  });
14054
14398
 
14055
- //# debugId=131DC57DB8537D5464756E2164756E21
14399
+ //# debugId=81FA5977F36B50C364756E2164756E21