claudemesh-cli 1.5.0 → 1.6.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.
@@ -88,7 +88,7 @@ __export(exports_urls, {
88
88
  VERSION: () => VERSION,
89
89
  URLS: () => URLS
90
90
  });
91
- var URLS, VERSION = "1.5.0", env;
91
+ var URLS, VERSION = "1.6.1", env;
92
92
  var init_urls = __esm(() => {
93
93
  URLS = {
94
94
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -751,10 +751,25 @@ function requireToken() {
751
751
  throw new NotSignedIn;
752
752
  return auth.session_token;
753
753
  }
754
+ function localView() {
755
+ const cfg = readConfig();
756
+ if (cfg.meshes.length === 0)
757
+ return;
758
+ return {
759
+ config_path: PATHS.CONFIG_FILE,
760
+ meshes: cfg.meshes.map((m) => ({
761
+ slug: m.slug,
762
+ mesh_id: m.meshId,
763
+ member_id: m.memberId,
764
+ pubkey_prefix: m.pubkey.slice(0, 12)
765
+ }))
766
+ };
767
+ }
754
768
  async function whoAmI() {
755
769
  const auth = getStoredToken();
770
+ const local = localView();
756
771
  if (!auth)
757
- return { signed_in: false };
772
+ return { signed_in: false, local };
758
773
  try {
759
774
  const profile = await exports_my.getProfile(auth.session_token);
760
775
  const meshes = await exports_my.getMeshes(auth.session_token);
@@ -763,12 +778,13 @@ async function whoAmI() {
763
778
  signed_in: true,
764
779
  user: profile,
765
780
  token_source: auth.token_source,
766
- meshes: { owned, guest: meshes.length - owned }
781
+ meshes: { owned, guest: meshes.length - owned },
782
+ local
767
783
  };
768
784
  } catch (err) {
769
785
  if (err instanceof ApiError && err.isUnauthorized) {
770
786
  clearToken();
771
- return { signed_in: false };
787
+ return { signed_in: false, local };
772
788
  }
773
789
  throw err;
774
790
  }
@@ -791,6 +807,8 @@ async function register(callbackPort) {
791
807
  var init_client2 = __esm(() => {
792
808
  init_facade3();
793
809
  init_facade3();
810
+ init_facade();
811
+ init_paths();
794
812
  init_token_store();
795
813
  init_errors2();
796
814
  });
@@ -1132,6 +1150,13 @@ class BrokerClient {
1132
1150
  grantFileAccessResolvers = new Map;
1133
1151
  peerFileResponseResolvers = new Map;
1134
1152
  peerDirResponseResolvers = new Map;
1153
+ topicCreatedResolvers = new Map;
1154
+ topicListResolvers = new Map;
1155
+ topicMembersResolvers = new Map;
1156
+ topicHistoryResolvers = new Map;
1157
+ apiKeyCreatedResolvers = new Map;
1158
+ apiKeyListResolvers = new Map;
1159
+ apiKeyRevokeResolvers = new Map;
1135
1160
  sharedDirs = [process.cwd()];
1136
1161
  _serviceCatalog = [];
1137
1162
  get serviceCatalog() {
@@ -1434,6 +1459,128 @@ class BrokerClient {
1434
1459
  return;
1435
1460
  this.ws.send(JSON.stringify({ type: "leave_group", name }));
1436
1461
  }
1462
+ async topicCreate(args) {
1463
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1464
+ return null;
1465
+ return new Promise((resolve) => {
1466
+ const reqId = this.makeReqId();
1467
+ this.topicCreatedResolvers.set(reqId, {
1468
+ resolve,
1469
+ timer: setTimeout(() => {
1470
+ if (this.topicCreatedResolvers.delete(reqId))
1471
+ resolve(null);
1472
+ }, 5000)
1473
+ });
1474
+ this.ws.send(JSON.stringify({ type: "topic_create", _reqId: reqId, ...args }));
1475
+ });
1476
+ }
1477
+ async topicList() {
1478
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1479
+ return [];
1480
+ return new Promise((resolve) => {
1481
+ const reqId = this.makeReqId();
1482
+ this.topicListResolvers.set(reqId, {
1483
+ resolve,
1484
+ timer: setTimeout(() => {
1485
+ if (this.topicListResolvers.delete(reqId))
1486
+ resolve([]);
1487
+ }, 5000)
1488
+ });
1489
+ this.ws.send(JSON.stringify({ type: "topic_list", _reqId: reqId }));
1490
+ });
1491
+ }
1492
+ async topicJoin(topic, role) {
1493
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1494
+ return;
1495
+ this.ws.send(JSON.stringify({ type: "topic_join", topic, role }));
1496
+ }
1497
+ async topicLeave(topic) {
1498
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1499
+ return;
1500
+ this.ws.send(JSON.stringify({ type: "topic_leave", topic }));
1501
+ }
1502
+ async topicMembers(topic) {
1503
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1504
+ return [];
1505
+ return new Promise((resolve) => {
1506
+ const reqId = this.makeReqId();
1507
+ this.topicMembersResolvers.set(reqId, {
1508
+ resolve,
1509
+ timer: setTimeout(() => {
1510
+ if (this.topicMembersResolvers.delete(reqId))
1511
+ resolve([]);
1512
+ }, 5000)
1513
+ });
1514
+ this.ws.send(JSON.stringify({ type: "topic_members", _reqId: reqId, topic }));
1515
+ });
1516
+ }
1517
+ async topicHistory(args) {
1518
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1519
+ return [];
1520
+ return new Promise((resolve) => {
1521
+ const reqId = this.makeReqId();
1522
+ this.topicHistoryResolvers.set(reqId, {
1523
+ resolve,
1524
+ timer: setTimeout(() => {
1525
+ if (this.topicHistoryResolvers.delete(reqId))
1526
+ resolve([]);
1527
+ }, 5000)
1528
+ });
1529
+ this.ws.send(JSON.stringify({ type: "topic_history", _reqId: reqId, ...args }));
1530
+ });
1531
+ }
1532
+ async topicMarkRead(topic) {
1533
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1534
+ return;
1535
+ this.ws.send(JSON.stringify({ type: "topic_mark_read", topic }));
1536
+ }
1537
+ async apiKeyCreate(args) {
1538
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1539
+ return null;
1540
+ return new Promise((resolve) => {
1541
+ const reqId = this.makeReqId();
1542
+ this.apiKeyCreatedResolvers.set(reqId, {
1543
+ resolve,
1544
+ timer: setTimeout(() => {
1545
+ if (this.apiKeyCreatedResolvers.delete(reqId))
1546
+ resolve(null);
1547
+ }, 5000)
1548
+ });
1549
+ this.ws.send(JSON.stringify({ type: "apikey_create", _reqId: reqId, ...args }));
1550
+ });
1551
+ }
1552
+ async apiKeyList() {
1553
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1554
+ return [];
1555
+ return new Promise((resolve) => {
1556
+ const reqId = this.makeReqId();
1557
+ this.apiKeyListResolvers.set(reqId, {
1558
+ resolve,
1559
+ timer: setTimeout(() => {
1560
+ if (this.apiKeyListResolvers.delete(reqId))
1561
+ resolve([]);
1562
+ }, 5000)
1563
+ });
1564
+ this.ws.send(JSON.stringify({ type: "apikey_list", _reqId: reqId }));
1565
+ });
1566
+ }
1567
+ async apiKeyRevoke(id) {
1568
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
1569
+ return { ok: false, code: "not_connected", message: "broker not connected" };
1570
+ }
1571
+ return new Promise((resolve) => {
1572
+ const reqId = this.makeReqId();
1573
+ this.apiKeyRevokeResolvers.set(reqId, {
1574
+ resolve,
1575
+ timer: setTimeout(() => {
1576
+ if (this.apiKeyRevokeResolvers.delete(reqId)) {
1577
+ resolve({ ok: false, code: "timeout", message: "broker did not respond within 5s" });
1578
+ }
1579
+ }, 5000)
1580
+ });
1581
+ this.ws.send(JSON.stringify({ type: "apikey_revoke", id, _reqId: reqId }));
1582
+ });
1583
+ }
1437
1584
  async setState(key, value) {
1438
1585
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
1439
1586
  return;
@@ -2514,6 +2661,65 @@ class BrokerClient {
2514
2661
  this.resolveFromMap(this.listPeersResolvers, msgReqId, peers);
2515
2662
  return;
2516
2663
  }
2664
+ if (msg.type === "topic_created") {
2665
+ const r = msg.topic ?? {};
2666
+ this.resolveFromMap(this.topicCreatedResolvers, msgReqId, {
2667
+ id: r.id,
2668
+ name: r.name,
2669
+ created: !!msg.created
2670
+ });
2671
+ return;
2672
+ }
2673
+ if (msg.type === "topic_list_response") {
2674
+ this.resolveFromMap(this.topicListResolvers, msgReqId, msg.topics ?? []);
2675
+ return;
2676
+ }
2677
+ if (msg.type === "topic_members_response") {
2678
+ this.resolveFromMap(this.topicMembersResolvers, msgReqId, msg.members ?? []);
2679
+ return;
2680
+ }
2681
+ if (msg.type === "topic_history_response") {
2682
+ this.resolveFromMap(this.topicHistoryResolvers, msgReqId, msg.messages ?? []);
2683
+ return;
2684
+ }
2685
+ if (msg.type === "apikey_created") {
2686
+ this.resolveFromMap(this.apiKeyCreatedResolvers, msgReqId, {
2687
+ id: String(msg.id ?? ""),
2688
+ secret: String(msg.secret ?? ""),
2689
+ label: String(msg.label ?? ""),
2690
+ prefix: String(msg.prefix ?? ""),
2691
+ capabilities: msg.capabilities ?? [],
2692
+ topicScopes: msg.topicScopes ?? null,
2693
+ createdAt: String(msg.createdAt ?? "")
2694
+ });
2695
+ return;
2696
+ }
2697
+ if (msg.type === "apikey_list_response") {
2698
+ this.resolveFromMap(this.apiKeyListResolvers, msgReqId, msg.keys ?? []);
2699
+ return;
2700
+ }
2701
+ if (msg.type === "apikey_revoke_response") {
2702
+ const status = String(msg.status ?? "");
2703
+ if (status === "revoked") {
2704
+ this.resolveFromMap(this.apiKeyRevokeResolvers, msgReqId, {
2705
+ ok: true,
2706
+ id: String(msg.id ?? "")
2707
+ });
2708
+ } else if (status === "not_found") {
2709
+ this.resolveFromMap(this.apiKeyRevokeResolvers, msgReqId, {
2710
+ ok: false,
2711
+ code: "not_found",
2712
+ message: "no api key matches that id in this mesh"
2713
+ });
2714
+ } else if (status === "not_unique") {
2715
+ this.resolveFromMap(this.apiKeyRevokeResolvers, msgReqId, {
2716
+ ok: false,
2717
+ code: "not_unique",
2718
+ message: `prefix matches ${Number(msg.matches ?? 0)} keys; use the full id`
2719
+ });
2720
+ }
2721
+ return;
2722
+ }
2517
2723
  if (msg.type === "push") {
2518
2724
  this._statsCounters.messagesIn++;
2519
2725
  const nonce = String(msg.nonce ?? "");
@@ -6598,7 +6804,17 @@ async function runSend(flags, to, message) {
6598
6804
  }
6599
6805
  await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
6600
6806
  let targetSpec = to;
6601
- if (!to.startsWith("@") && to !== "*" && !/^[0-9a-f]{64}$/i.test(to)) {
6807
+ if (to.startsWith("#") && !/^#[0-9a-z_-]{20,}$/i.test(to)) {
6808
+ const name = to.slice(1);
6809
+ const topics = await client.topicList();
6810
+ const match = topics.find((t) => t.name === name);
6811
+ if (!match) {
6812
+ const names = topics.map((t) => "#" + t.name).join(", ");
6813
+ render.err(`Topic "${to}" not found.`, `topics: ${names || "(none)"}`);
6814
+ process.exit(1);
6815
+ }
6816
+ targetSpec = "#" + match.id;
6817
+ } else if (!to.startsWith("@") && !to.startsWith("#") && to !== "*" && !/^[0-9a-f]{64}$/i.test(to)) {
6602
6818
  const peers = await client.listPeers();
6603
6819
  const match = peers.find((p) => p.displayName.toLowerCase() === to.toLowerCase());
6604
6820
  if (!match) {
@@ -7358,18 +7574,33 @@ async function whoami(opts) {
7358
7574
  const result = await whoAmI();
7359
7575
  if (opts.json) {
7360
7576
  console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2));
7361
- return EXIT.SUCCESS;
7577
+ return result.signed_in || result.local ? EXIT.SUCCESS : EXIT.AUTH_FAILED;
7362
7578
  }
7363
- if (!result.signed_in) {
7364
- render.err("Not signed in", "Run `claudemesh login` to sign in.");
7579
+ if (!result.signed_in && !result.local) {
7580
+ render.err("Not signed in", "Run `claudemesh login` to sign in or `claudemesh <invite>` to join.");
7365
7581
  return EXIT.AUTH_FAILED;
7366
7582
  }
7367
7583
  render.section("whoami");
7368
- render.kv([
7369
- ["user", `${bold(result.user.display_name)} ${dim(`(${result.user.email})`)}`],
7370
- ["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
7371
- ...result.meshes ? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`]] : []
7372
- ]);
7584
+ if (result.signed_in) {
7585
+ render.kv([
7586
+ ["user", `${bold(result.user.display_name)} ${dim(`(${result.user.email})`)}`],
7587
+ ["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
7588
+ ...result.meshes ? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`]] : []
7589
+ ]);
7590
+ } else {
7591
+ render.kv([
7592
+ ["web", dim("not signed in · run `claudemesh login` for account features")]
7593
+ ]);
7594
+ }
7595
+ if (result.local) {
7596
+ render.blank();
7597
+ render.kv([
7598
+ ["local", `${result.local.meshes.length} mesh${result.local.meshes.length === 1 ? "" : "es"} · ${dim(result.local.config_path)}`]
7599
+ ]);
7600
+ for (const m of result.local.meshes) {
7601
+ console.log(` ${clay("●")} ${bold(m.slug)} ${dim(`member ${m.member_id.slice(0, 8)}… pk ${m.pubkey_prefix}…`)}`);
7602
+ }
7603
+ }
7373
7604
  render.blank();
7374
7605
  return EXIT.SUCCESS;
7375
7606
  }
@@ -10378,6 +10609,1045 @@ var init_platform_actions = __esm(() => {
10378
10609
  init_exit_codes();
10379
10610
  });
10380
10611
 
10612
+ // ../../packages/sdk/dist/crypto.js
10613
+ var require_crypto = __commonJS((exports) => {
10614
+ var __importDefault = exports && exports.__importDefault || function(mod) {
10615
+ return mod && mod.__esModule ? mod : { default: mod };
10616
+ };
10617
+ Object.defineProperty(exports, "__esModule", { value: true });
10618
+ exports.generateKeyPair = generateKeyPair;
10619
+ exports.signHello = signHello2;
10620
+ exports.isDirectTarget = isDirectTarget2;
10621
+ exports.encryptDirect = encryptDirect2;
10622
+ exports.decryptDirect = decryptDirect2;
10623
+ var libsodium_wrappers_1 = __importDefault(__require("libsodium-wrappers"));
10624
+ var ready2 = false;
10625
+ async function ensureSodium3() {
10626
+ if (!ready2) {
10627
+ await libsodium_wrappers_1.default.ready;
10628
+ ready2 = true;
10629
+ }
10630
+ return libsodium_wrappers_1.default;
10631
+ }
10632
+ async function generateKeyPair() {
10633
+ const s = await ensureSodium3();
10634
+ const kp = s.crypto_sign_keypair();
10635
+ return {
10636
+ publicKey: s.to_hex(kp.publicKey),
10637
+ secretKey: s.to_hex(kp.privateKey)
10638
+ };
10639
+ }
10640
+ async function signHello2(meshId, memberId, pubkey, secretKeyHex) {
10641
+ const s = await ensureSodium3();
10642
+ const timestamp2 = Date.now();
10643
+ const canonical = `${meshId}|${memberId}|${pubkey}|${timestamp2}`;
10644
+ const sig = s.crypto_sign_detached(s.from_string(canonical), s.from_hex(secretKeyHex));
10645
+ return { timestamp: timestamp2, signature: s.to_hex(sig) };
10646
+ }
10647
+ var HEX_PUBKEY2 = /^[0-9a-f]{64}$/;
10648
+ function isDirectTarget2(targetSpec) {
10649
+ return HEX_PUBKEY2.test(targetSpec);
10650
+ }
10651
+ async function encryptDirect2(message, recipientPubkeyHex, senderSecretKeyHex) {
10652
+ const s = await ensureSodium3();
10653
+ const recipientPub = s.crypto_sign_ed25519_pk_to_curve25519(s.from_hex(recipientPubkeyHex));
10654
+ const senderSec = s.crypto_sign_ed25519_sk_to_curve25519(s.from_hex(senderSecretKeyHex));
10655
+ const nonce = s.randombytes_buf(s.crypto_box_NONCEBYTES);
10656
+ const ciphertext = s.crypto_box_easy(s.from_string(message), nonce, recipientPub, senderSec);
10657
+ return {
10658
+ nonce: s.to_base64(nonce, s.base64_variants.ORIGINAL),
10659
+ ciphertext: s.to_base64(ciphertext, s.base64_variants.ORIGINAL)
10660
+ };
10661
+ }
10662
+ async function decryptDirect2(envelope, senderPubkeyHex, recipientSecretKeyHex) {
10663
+ const s = await ensureSodium3();
10664
+ try {
10665
+ const senderPub = s.crypto_sign_ed25519_pk_to_curve25519(s.from_hex(senderPubkeyHex));
10666
+ const recipientSec = s.crypto_sign_ed25519_sk_to_curve25519(s.from_hex(recipientSecretKeyHex));
10667
+ const nonce = s.from_base64(envelope.nonce, s.base64_variants.ORIGINAL);
10668
+ const ciphertext = s.from_base64(envelope.ciphertext, s.base64_variants.ORIGINAL);
10669
+ const plain = s.crypto_box_open_easy(ciphertext, nonce, senderPub, recipientSec);
10670
+ return s.to_string(plain);
10671
+ } catch {
10672
+ return null;
10673
+ }
10674
+ }
10675
+ });
10676
+
10677
+ // ../../packages/sdk/dist/client.js
10678
+ var require_client = __commonJS((exports) => {
10679
+ var __importDefault = exports && exports.__importDefault || function(mod) {
10680
+ return mod && mod.__esModule ? mod : { default: mod };
10681
+ };
10682
+ Object.defineProperty(exports, "__esModule", { value: true });
10683
+ exports.MeshClient = undefined;
10684
+ var node_events_1 = __require("node:events");
10685
+ var node_crypto_1 = __require("node:crypto");
10686
+ var ws_1 = __importDefault(__require("ws"));
10687
+ var crypto_js_1 = require_crypto();
10688
+ var MAX_QUEUED2 = 100;
10689
+ var HELLO_ACK_TIMEOUT_MS2 = 5000;
10690
+ var BACKOFF_CAPS2 = [1000, 2000, 4000, 8000, 16000, 30000];
10691
+
10692
+ class MeshClient extends node_events_1.EventEmitter {
10693
+ opts;
10694
+ ws = null;
10695
+ _status = "closed";
10696
+ pendingSends = new Map;
10697
+ outbound = [];
10698
+ closed = false;
10699
+ reconnectAttempt = 0;
10700
+ helloTimer = null;
10701
+ reconnectTimer = null;
10702
+ sessionPubkey = null;
10703
+ sessionSecretKey = null;
10704
+ listPeersResolvers = new Map;
10705
+ stateResolvers = new Map;
10706
+ constructor(opts) {
10707
+ super();
10708
+ this.opts = opts;
10709
+ }
10710
+ get status() {
10711
+ return this._status;
10712
+ }
10713
+ get pubkey() {
10714
+ return this.sessionPubkey;
10715
+ }
10716
+ async connect() {
10717
+ if (this.closed)
10718
+ throw new Error("client is closed");
10719
+ this._status = "connecting";
10720
+ const ws = new ws_1.default(this.opts.brokerUrl);
10721
+ this.ws = ws;
10722
+ return new Promise((resolve3, reject) => {
10723
+ const onOpen = async () => {
10724
+ this.debug("ws open -> generating session keypair + signing hello");
10725
+ try {
10726
+ if (!this.sessionPubkey) {
10727
+ const sessionKP = await (0, crypto_js_1.generateKeyPair)();
10728
+ this.sessionPubkey = sessionKP.publicKey;
10729
+ this.sessionSecretKey = sessionKP.secretKey;
10730
+ }
10731
+ const { timestamp: timestamp2, signature } = await (0, crypto_js_1.signHello)(this.opts.meshId, this.opts.memberId, this.opts.pubkey, this.opts.secretKey);
10732
+ ws.send(JSON.stringify({
10733
+ type: "hello",
10734
+ meshId: this.opts.meshId,
10735
+ memberId: this.opts.memberId,
10736
+ pubkey: this.opts.pubkey,
10737
+ sessionPubkey: this.sessionPubkey,
10738
+ displayName: this.opts.displayName,
10739
+ sessionId: `sdk-${process.pid}-${Date.now()}`,
10740
+ pid: process.pid,
10741
+ peerType: this.opts.peerType ?? "connector",
10742
+ channel: this.opts.channel ?? "sdk",
10743
+ timestamp: timestamp2,
10744
+ signature
10745
+ }));
10746
+ } catch (e) {
10747
+ reject(new Error(`hello sign failed: ${e instanceof Error ? e.message : e}`));
10748
+ return;
10749
+ }
10750
+ this.helloTimer = setTimeout(() => {
10751
+ this.debug("hello_ack timeout");
10752
+ ws.close();
10753
+ reject(new Error("hello_ack timeout"));
10754
+ }, HELLO_ACK_TIMEOUT_MS2);
10755
+ };
10756
+ const onMessage = (raw) => {
10757
+ let msg;
10758
+ try {
10759
+ msg = JSON.parse(raw.toString());
10760
+ } catch {
10761
+ return;
10762
+ }
10763
+ if (msg.type === "hello_ack") {
10764
+ if (this.helloTimer)
10765
+ clearTimeout(this.helloTimer);
10766
+ this.helloTimer = null;
10767
+ this._status = "open";
10768
+ this.reconnectAttempt = 0;
10769
+ this.flushOutbound();
10770
+ this.emit("connected");
10771
+ resolve3();
10772
+ return;
10773
+ }
10774
+ this.handleServerMessage(msg);
10775
+ };
10776
+ const onClose = () => {
10777
+ if (this.helloTimer)
10778
+ clearTimeout(this.helloTimer);
10779
+ this.helloTimer = null;
10780
+ const wasOpen = this._status === "open" || this._status === "reconnecting";
10781
+ this.ws = null;
10782
+ if (!wasOpen && this._status === "connecting") {
10783
+ reject(new Error("ws closed before hello_ack"));
10784
+ }
10785
+ if (!this.closed) {
10786
+ this.emit("disconnected");
10787
+ this.scheduleReconnect();
10788
+ } else {
10789
+ this._status = "closed";
10790
+ this.emit("disconnected");
10791
+ }
10792
+ };
10793
+ const onError = (err) => {
10794
+ this.debug(`ws error: ${err.message}`);
10795
+ };
10796
+ ws.on("open", onOpen);
10797
+ ws.on("message", onMessage);
10798
+ ws.on("close", onClose);
10799
+ ws.on("error", onError);
10800
+ });
10801
+ }
10802
+ disconnect() {
10803
+ this.closed = true;
10804
+ if (this.helloTimer)
10805
+ clearTimeout(this.helloTimer);
10806
+ if (this.reconnectTimer)
10807
+ clearTimeout(this.reconnectTimer);
10808
+ if (this.ws) {
10809
+ try {
10810
+ this.ws.close();
10811
+ } catch {}
10812
+ }
10813
+ this._status = "closed";
10814
+ }
10815
+ async send(to, message, priority = "next") {
10816
+ let targetSpec = to;
10817
+ if (!(0, crypto_js_1.isDirectTarget)(to) && to !== "*" && !to.startsWith("@") && !to.startsWith("#")) {
10818
+ const peers = await this.listPeers();
10819
+ const match = peers.find((p) => p.displayName.toLowerCase() === to.toLowerCase());
10820
+ if (match) {
10821
+ targetSpec = match.pubkey;
10822
+ }
10823
+ }
10824
+ const id = (0, node_crypto_1.randomBytes)(8).toString("hex");
10825
+ let nonce;
10826
+ let ciphertext;
10827
+ if ((0, crypto_js_1.isDirectTarget)(targetSpec)) {
10828
+ const env2 = await (0, crypto_js_1.encryptDirect)(message, targetSpec, this.sessionSecretKey ?? this.opts.secretKey);
10829
+ nonce = env2.nonce;
10830
+ ciphertext = env2.ciphertext;
10831
+ } else {
10832
+ nonce = (0, node_crypto_1.randomBytes)(24).toString("base64");
10833
+ ciphertext = Buffer.from(message, "utf-8").toString("base64");
10834
+ }
10835
+ return new Promise((resolve3) => {
10836
+ if (this.pendingSends.size >= MAX_QUEUED2) {
10837
+ resolve3({ ok: false, error: "outbound queue full" });
10838
+ return;
10839
+ }
10840
+ this.pendingSends.set(id, {
10841
+ id,
10842
+ targetSpec,
10843
+ priority,
10844
+ nonce,
10845
+ ciphertext,
10846
+ resolve: resolve3
10847
+ });
10848
+ const dispatch = () => {
10849
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10850
+ return;
10851
+ this.ws.send(JSON.stringify({
10852
+ type: "send",
10853
+ id,
10854
+ targetSpec,
10855
+ priority,
10856
+ nonce,
10857
+ ciphertext
10858
+ }));
10859
+ };
10860
+ if (this._status === "open")
10861
+ dispatch();
10862
+ else {
10863
+ if (this.outbound.length >= MAX_QUEUED2) {
10864
+ this.pendingSends.delete(id);
10865
+ resolve3({ ok: false, error: "outbound queue full" });
10866
+ return;
10867
+ }
10868
+ this.outbound.push(dispatch);
10869
+ }
10870
+ setTimeout(() => {
10871
+ if (this.pendingSends.has(id)) {
10872
+ this.pendingSends.delete(id);
10873
+ resolve3({ ok: false, error: "ack timeout" });
10874
+ }
10875
+ }, 1e4);
10876
+ });
10877
+ }
10878
+ async broadcast(message, priority = "next") {
10879
+ return this.send("*", message, priority);
10880
+ }
10881
+ async listPeers() {
10882
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10883
+ return [];
10884
+ return new Promise((resolve3) => {
10885
+ const reqId = this.makeReqId();
10886
+ this.listPeersResolvers.set(reqId, {
10887
+ resolve: resolve3,
10888
+ timer: setTimeout(() => {
10889
+ if (this.listPeersResolvers.delete(reqId))
10890
+ resolve3([]);
10891
+ }, 5000)
10892
+ });
10893
+ this.ws.send(JSON.stringify({ type: "list_peers", _reqId: reqId }));
10894
+ });
10895
+ }
10896
+ async getState(key) {
10897
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10898
+ return null;
10899
+ return new Promise((resolve3) => {
10900
+ const reqId = this.makeReqId();
10901
+ this.stateResolvers.set(reqId, {
10902
+ resolve: (result) => resolve3(result ? String(result.value) : null),
10903
+ timer: setTimeout(() => {
10904
+ if (this.stateResolvers.delete(reqId))
10905
+ resolve3(null);
10906
+ }, 5000)
10907
+ });
10908
+ this.ws.send(JSON.stringify({ type: "get_state", key, _reqId: reqId }));
10909
+ });
10910
+ }
10911
+ async setState(key, value) {
10912
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10913
+ return;
10914
+ this.ws.send(JSON.stringify({ type: "set_state", key, value }));
10915
+ }
10916
+ async setSummary(summary) {
10917
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10918
+ return;
10919
+ this.ws.send(JSON.stringify({ type: "set_summary", summary }));
10920
+ }
10921
+ async setStatus(status) {
10922
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10923
+ return;
10924
+ this.ws.send(JSON.stringify({ type: "set_status", status }));
10925
+ }
10926
+ async createTopic(args) {
10927
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10928
+ return;
10929
+ this.ws.send(JSON.stringify({ type: "topic_create", ...args }));
10930
+ }
10931
+ async joinTopic(topic, role) {
10932
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10933
+ return;
10934
+ this.ws.send(JSON.stringify({ type: "topic_join", topic, role }));
10935
+ }
10936
+ async leaveTopic(topic) {
10937
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
10938
+ return;
10939
+ this.ws.send(JSON.stringify({ type: "topic_leave", topic }));
10940
+ }
10941
+ makeReqId() {
10942
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
10943
+ }
10944
+ flushOutbound() {
10945
+ const queued = this.outbound.slice();
10946
+ this.outbound.length = 0;
10947
+ for (const send of queued)
10948
+ send();
10949
+ }
10950
+ scheduleReconnect() {
10951
+ this._status = "reconnecting";
10952
+ const delay = BACKOFF_CAPS2[Math.min(this.reconnectAttempt, BACKOFF_CAPS2.length - 1)];
10953
+ this.reconnectAttempt += 1;
10954
+ this.debug(`reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`);
10955
+ this.reconnectTimer = setTimeout(() => {
10956
+ if (this.closed)
10957
+ return;
10958
+ this.connect().catch((e) => {
10959
+ this.debug(`reconnect failed: ${e instanceof Error ? e.message : e}`);
10960
+ });
10961
+ }, delay);
10962
+ }
10963
+ handleServerMessage(msg) {
10964
+ const reqId = msg._reqId;
10965
+ if (msg.type === "ack") {
10966
+ const pending = this.pendingSends.get(String(msg.id ?? ""));
10967
+ if (pending) {
10968
+ pending.resolve({
10969
+ ok: true,
10970
+ messageId: String(msg.messageId ?? "")
10971
+ });
10972
+ this.pendingSends.delete(pending.id);
10973
+ }
10974
+ return;
10975
+ }
10976
+ if (msg.type === "peers_list") {
10977
+ const peers = msg.peers ?? [];
10978
+ this.resolveFromMap(this.listPeersResolvers, reqId, peers);
10979
+ return;
10980
+ }
10981
+ if (msg.type === "push") {
10982
+ this.handlePush(msg);
10983
+ return;
10984
+ }
10985
+ if (msg.type === "state_result") {
10986
+ if (msg.key) {
10987
+ this.resolveFromMap(this.stateResolvers, reqId, {
10988
+ key: String(msg.key),
10989
+ value: msg.value,
10990
+ updatedBy: String(msg.updatedBy ?? ""),
10991
+ updatedAt: String(msg.updatedAt ?? "")
10992
+ });
10993
+ } else {
10994
+ this.resolveFromMap(this.stateResolvers, reqId, null);
10995
+ }
10996
+ return;
10997
+ }
10998
+ if (msg.type === "state_change") {
10999
+ this.emit("state_change", {
11000
+ key: String(msg.key ?? ""),
11001
+ value: msg.value,
11002
+ updatedBy: String(msg.updatedBy ?? "")
11003
+ });
11004
+ return;
11005
+ }
11006
+ if (msg.type === "error") {
11007
+ this.debug(`broker error: ${msg.code} ${msg.message}`);
11008
+ const id = msg.id ? String(msg.id) : null;
11009
+ if (id) {
11010
+ const pending = this.pendingSends.get(id);
11011
+ if (pending) {
11012
+ pending.resolve({
11013
+ ok: false,
11014
+ error: `${msg.code}: ${msg.message}`
11015
+ });
11016
+ this.pendingSends.delete(id);
11017
+ }
11018
+ }
11019
+ return;
11020
+ }
11021
+ }
11022
+ async handlePush(msg) {
11023
+ const nonce = String(msg.nonce ?? "");
11024
+ const ciphertext = String(msg.ciphertext ?? "");
11025
+ const senderPubkey = String(msg.senderPubkey ?? "");
11026
+ const kind = senderPubkey ? "direct" : "unknown";
11027
+ let plaintext = null;
11028
+ if (senderPubkey && nonce && ciphertext) {
11029
+ plaintext = await (0, crypto_js_1.decryptDirect)({ nonce, ciphertext }, senderPubkey, this.sessionSecretKey ?? this.opts.secretKey);
11030
+ }
11031
+ if (plaintext === null && ciphertext && !senderPubkey) {
11032
+ try {
11033
+ plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
11034
+ } catch {
11035
+ plaintext = null;
11036
+ }
11037
+ }
11038
+ if (plaintext === null && ciphertext) {
11039
+ try {
11040
+ const decoded = Buffer.from(ciphertext, "base64").toString("utf-8");
11041
+ if (/^[\x20-\x7E\s\u00A0-\uFFFF]*$/.test(decoded) && decoded.length > 0) {
11042
+ plaintext = decoded;
11043
+ }
11044
+ } catch {
11045
+ plaintext = null;
11046
+ }
11047
+ }
11048
+ const push = {
11049
+ messageId: String(msg.messageId ?? ""),
11050
+ meshId: String(msg.meshId ?? ""),
11051
+ senderPubkey,
11052
+ priority: msg.priority ?? "next",
11053
+ nonce,
11054
+ ciphertext,
11055
+ createdAt: String(msg.createdAt ?? ""),
11056
+ receivedAt: new Date().toISOString(),
11057
+ plaintext,
11058
+ kind,
11059
+ ...msg.subtype ? { subtype: msg.subtype } : {},
11060
+ ...msg.event ? { event: String(msg.event) } : {},
11061
+ ...msg.eventData ? { eventData: msg.eventData } : {}
11062
+ };
11063
+ this.emit("message", push);
11064
+ if (push.event === "peer_joined" && push.eventData) {
11065
+ this.emit("peer_joined", push.eventData);
11066
+ }
11067
+ if (push.event === "peer_left" && push.eventData) {
11068
+ this.emit("peer_left", push.eventData);
11069
+ }
11070
+ }
11071
+ resolveFromMap(map, reqId, value) {
11072
+ let entry = reqId ? map.get(reqId) : undefined;
11073
+ if (!entry) {
11074
+ const first = map.entries().next().value;
11075
+ if (first) {
11076
+ entry = first[1];
11077
+ map.delete(first[0]);
11078
+ }
11079
+ } else {
11080
+ map.delete(reqId);
11081
+ }
11082
+ if (entry) {
11083
+ clearTimeout(entry.timer);
11084
+ entry.resolve(value);
11085
+ return true;
11086
+ }
11087
+ return false;
11088
+ }
11089
+ debug(msg) {
11090
+ if (this.opts.debug)
11091
+ console.error(`[claudemesh-sdk] ${msg}`);
11092
+ }
11093
+ }
11094
+ exports.MeshClient = MeshClient;
11095
+ });
11096
+
11097
+ // ../../packages/sdk/dist/bridge.js
11098
+ var require_bridge = __commonJS((exports) => {
11099
+ Object.defineProperty(exports, "__esModule", { value: true });
11100
+ exports.Bridge = undefined;
11101
+ var node_events_1 = __require("node:events");
11102
+ var client_js_1 = require_client();
11103
+ var HOP_PREFIX_RE = /^__cmh(\d+):/;
11104
+ var MAX_HOPS_DEFAULT = 2;
11105
+
11106
+ class Bridge extends node_events_1.EventEmitter {
11107
+ clientA;
11108
+ clientB;
11109
+ maxHops;
11110
+ opts;
11111
+ started = false;
11112
+ constructor(opts) {
11113
+ super();
11114
+ this.opts = opts;
11115
+ this.maxHops = opts.maxHops ?? MAX_HOPS_DEFAULT;
11116
+ this.clientA = new client_js_1.MeshClient(opts.a.client);
11117
+ this.clientB = new client_js_1.MeshClient(opts.b.client);
11118
+ }
11119
+ async start() {
11120
+ if (this.started)
11121
+ return;
11122
+ this.started = true;
11123
+ await Promise.all([this.clientA.connect(), this.clientB.connect()]);
11124
+ await Promise.all([
11125
+ this.clientA.joinTopic(this.opts.a.topic, this.opts.a.role),
11126
+ this.clientB.joinTopic(this.opts.b.topic, this.opts.b.role)
11127
+ ]);
11128
+ this.clientA.on("message", (m) => this.handleIncoming("a", m).catch((e) => this.emit("error", e instanceof Error ? e : new Error(String(e)))));
11129
+ this.clientB.on("message", (m) => this.handleIncoming("b", m).catch((e) => this.emit("error", e instanceof Error ? e : new Error(String(e)))));
11130
+ }
11131
+ async stop() {
11132
+ if (!this.started)
11133
+ return;
11134
+ this.started = false;
11135
+ this.clientA.disconnect();
11136
+ this.clientB.disconnect();
11137
+ }
11138
+ async handleIncoming(fromSide, msg) {
11139
+ if (msg.subtype === "system")
11140
+ return;
11141
+ const text = msg.plaintext;
11142
+ if (!text)
11143
+ return;
11144
+ const ownA = this.clientA.pubkey;
11145
+ const ownB = this.clientB.pubkey;
11146
+ if (msg.senderPubkey === ownA || msg.senderPubkey === ownB) {
11147
+ this.emit("dropped", { from: fromSide, reason: "echo", hop: -1 });
11148
+ return;
11149
+ }
11150
+ if (this.opts.filter) {
11151
+ const ok = await this.opts.filter(msg, fromSide);
11152
+ if (!ok) {
11153
+ this.emit("dropped", { from: fromSide, reason: "filter", hop: -1 });
11154
+ return;
11155
+ }
11156
+ }
11157
+ const m = text.match(HOP_PREFIX_RE);
11158
+ const currentHop = m ? Number(m[1]) : 0;
11159
+ const nextHop = currentHop + 1;
11160
+ if (nextHop > this.maxHops) {
11161
+ this.emit("dropped", { from: fromSide, reason: "max_hops", hop: currentHop });
11162
+ return;
11163
+ }
11164
+ const stripped = m ? text.slice(m[0].length) : text;
11165
+ const forwarded = `__cmh${nextHop}:${stripped}`;
11166
+ const targetClient = fromSide === "a" ? this.clientB : this.clientA;
11167
+ const targetTopic = fromSide === "a" ? this.opts.b.topic : this.opts.a.topic;
11168
+ await targetClient.send(`#${targetTopic}`, forwarded, "next");
11169
+ this.emit("forwarded", {
11170
+ from: fromSide,
11171
+ to: fromSide === "a" ? "b" : "a",
11172
+ hop: nextHop,
11173
+ bytes: forwarded.length
11174
+ });
11175
+ }
11176
+ }
11177
+ exports.Bridge = Bridge;
11178
+ });
11179
+
11180
+ // ../../packages/sdk/dist/index.js
11181
+ var require_dist = __commonJS((exports) => {
11182
+ Object.defineProperty(exports, "__esModule", { value: true });
11183
+ exports.Bridge = exports.generateKeyPair = exports.MeshClient = undefined;
11184
+ var client_js_1 = require_client();
11185
+ Object.defineProperty(exports, "MeshClient", { enumerable: true, get: function() {
11186
+ return client_js_1.MeshClient;
11187
+ } });
11188
+ var crypto_js_1 = require_crypto();
11189
+ Object.defineProperty(exports, "generateKeyPair", { enumerable: true, get: function() {
11190
+ return crypto_js_1.generateKeyPair;
11191
+ } });
11192
+ var bridge_js_1 = require_bridge();
11193
+ Object.defineProperty(exports, "Bridge", { enumerable: true, get: function() {
11194
+ return bridge_js_1.Bridge;
11195
+ } });
11196
+ });
11197
+
11198
+ // src/commands/bridge.ts
11199
+ var exports_bridge = {};
11200
+ __export(exports_bridge, {
11201
+ runBridge: () => runBridge,
11202
+ bridgeConfigTemplate: () => bridgeConfigTemplate
11203
+ });
11204
+ import { readFileSync as readFileSync14, existsSync as existsSync20 } from "node:fs";
11205
+ function parseConfig(text) {
11206
+ const trimmed = text.trim();
11207
+ if (trimmed.startsWith("{"))
11208
+ return JSON.parse(trimmed);
11209
+ const root = {};
11210
+ let cursor = null;
11211
+ for (const raw of text.split(`
11212
+ `)) {
11213
+ const line = raw.replace(/#.*$/, "").trimEnd();
11214
+ if (!line.trim())
11215
+ continue;
11216
+ const top = line.match(/^(a|b)\s*:\s*$/);
11217
+ if (top) {
11218
+ cursor = {};
11219
+ root[top[1]] = cursor;
11220
+ continue;
11221
+ }
11222
+ const flat = line.match(/^(\w+)\s*:\s*(.+)$/);
11223
+ if (flat && /^\s/.test(line) && cursor) {
11224
+ cursor[flat[1]] = parseScalar(flat[2]);
11225
+ } else if (flat) {
11226
+ const v = parseScalar(flat[2]);
11227
+ if (typeof v === "number")
11228
+ root[flat[1]] = v;
11229
+ }
11230
+ }
11231
+ return root;
11232
+ }
11233
+ function parseScalar(raw) {
11234
+ const v = raw.trim().replace(/^["'](.*)["']$/, "$1");
11235
+ if (v === "true")
11236
+ return true;
11237
+ if (v === "false")
11238
+ return false;
11239
+ if (/^-?\d+(\.\d+)?$/.test(v))
11240
+ return Number(v);
11241
+ return v;
11242
+ }
11243
+ async function runBridge(configPath) {
11244
+ if (!configPath) {
11245
+ render.err("Usage: claudemesh bridge run <config.yaml>");
11246
+ return EXIT.INVALID_ARGS;
11247
+ }
11248
+ if (!existsSync20(configPath)) {
11249
+ render.err(`config file not found: ${configPath}`);
11250
+ return EXIT.NOT_FOUND;
11251
+ }
11252
+ let cfg;
11253
+ try {
11254
+ cfg = parseConfig(readFileSync14(configPath, "utf-8"));
11255
+ } catch (e) {
11256
+ render.err(`failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
11257
+ return EXIT.INVALID_ARGS;
11258
+ }
11259
+ if (!cfg.a || !cfg.b) {
11260
+ render.err("config must define 'a:' and 'b:' sections");
11261
+ return EXIT.INVALID_ARGS;
11262
+ }
11263
+ for (const [name, side] of [["a", cfg.a], ["b", cfg.b]]) {
11264
+ if (!side.broker_url || !side.mesh_id || !side.member_id || !side.pubkey || !side.secret_key || !side.topic) {
11265
+ render.err(`config side '${name}' missing required fields: broker_url, mesh_id, member_id, pubkey, secret_key, topic`);
11266
+ return EXIT.INVALID_ARGS;
11267
+ }
11268
+ }
11269
+ const { Bridge } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
11270
+ const bridge = new Bridge({
11271
+ a: {
11272
+ client: {
11273
+ brokerUrl: cfg.a.broker_url,
11274
+ meshId: cfg.a.mesh_id,
11275
+ memberId: cfg.a.member_id,
11276
+ pubkey: cfg.a.pubkey,
11277
+ secretKey: cfg.a.secret_key,
11278
+ displayName: cfg.a.display_name ?? "bridge",
11279
+ peerType: "connector",
11280
+ channel: "bridge"
11281
+ },
11282
+ topic: cfg.a.topic,
11283
+ role: cfg.a.role
11284
+ },
11285
+ b: {
11286
+ client: {
11287
+ brokerUrl: cfg.b.broker_url,
11288
+ meshId: cfg.b.mesh_id,
11289
+ memberId: cfg.b.member_id,
11290
+ pubkey: cfg.b.pubkey,
11291
+ secretKey: cfg.b.secret_key,
11292
+ displayName: cfg.b.display_name ?? "bridge",
11293
+ peerType: "connector",
11294
+ channel: "bridge"
11295
+ },
11296
+ topic: cfg.b.topic,
11297
+ role: cfg.b.role
11298
+ },
11299
+ maxHops: cfg.max_hops
11300
+ });
11301
+ bridge.on("forwarded", (e) => {
11302
+ process.stdout.write(`${dim(new Date().toISOString())} ${green("→")} ${e.from}→${e.to} hop=${e.hop} ${dim(`${e.bytes}b`)}
11303
+ `);
11304
+ });
11305
+ bridge.on("dropped", (e) => {
11306
+ process.stdout.write(`${dim(new Date().toISOString())} ${yellow("·")} drop from=${e.from} reason=${e.reason}${e.hop >= 0 ? ` hop=${e.hop}` : ""}
11307
+ `);
11308
+ });
11309
+ bridge.on("error", (e) => {
11310
+ process.stderr.write(`${red("✘")} ${e.message}
11311
+ `);
11312
+ });
11313
+ try {
11314
+ await bridge.start();
11315
+ } catch (e) {
11316
+ render.err(`bridge failed to start: ${e instanceof Error ? e.message : String(e)}`);
11317
+ return EXIT.NETWORK_ERROR;
11318
+ }
11319
+ render.ok("bridge running", `${clay("#" + cfg.a.topic)} ${dim("⟷")} ${clay("#" + cfg.b.topic)}`);
11320
+ process.stderr.write(`${dim(` meshes: ${cfg.a.mesh_id.slice(0, 8)} ⟷ ${cfg.b.mesh_id.slice(0, 8)} max_hops: ${cfg.max_hops ?? 2}`)}
11321
+ `);
11322
+ process.stderr.write(`${dim(" Ctrl-C to stop.")}
11323
+
11324
+ `);
11325
+ await new Promise((resolve3) => {
11326
+ const stop = async () => {
11327
+ process.stderr.write(`
11328
+ ${dim("stopping bridge...")}
11329
+ `);
11330
+ await bridge.stop();
11331
+ resolve3();
11332
+ };
11333
+ process.on("SIGINT", stop);
11334
+ process.on("SIGTERM", stop);
11335
+ });
11336
+ return EXIT.SUCCESS;
11337
+ }
11338
+ function bridgeConfigTemplate() {
11339
+ return `# claudemesh bridge config
11340
+ # Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
11341
+ #
11342
+ # A bridge holds memberships in two meshes and forwards messages on a
11343
+ # single topic between them. Loop prevention via plaintext hop counter
11344
+ # (visible in message body — minor wart, fixed in v0.3.0).
11345
+ #
11346
+ # Tip: \`claudemesh peer verify\` shows the keys/ids you need below.
11347
+
11348
+ max_hops: 2
11349
+
11350
+ a:
11351
+ broker_url: wss://ic.claudemesh.com/ws
11352
+ mesh_id: <mesh A id>
11353
+ member_id: <bridge member id in mesh A>
11354
+ pubkey: <ed25519 public key hex, 32 bytes>
11355
+ secret_key: <ed25519 secret key hex, 64 bytes>
11356
+ topic: incidents
11357
+ display_name: bridge
11358
+ role: member
11359
+
11360
+ b:
11361
+ broker_url: wss://ic.claudemesh.com/ws
11362
+ mesh_id: <mesh B id>
11363
+ member_id: <bridge member id in mesh B>
11364
+ pubkey: <ed25519 public key hex>
11365
+ secret_key: <ed25519 secret key hex>
11366
+ topic: incidents
11367
+ `;
11368
+ }
11369
+ var init_bridge = __esm(() => {
11370
+ init_render();
11371
+ init_styles();
11372
+ init_exit_codes();
11373
+ });
11374
+
11375
+ // src/commands/apikey.ts
11376
+ var exports_apikey = {};
11377
+ __export(exports_apikey, {
11378
+ runApiKeyRevoke: () => runApiKeyRevoke,
11379
+ runApiKeyList: () => runApiKeyList,
11380
+ runApiKeyCreate: () => runApiKeyCreate
11381
+ });
11382
+ function parseCapabilities(raw) {
11383
+ if (!raw)
11384
+ return ["send", "read"];
11385
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
11386
+ const valid = new Set(["send", "read", "state_write", "admin"]);
11387
+ return parts.filter((p) => valid.has(p));
11388
+ }
11389
+ async function runApiKeyCreate(label, flags) {
11390
+ if (!label) {
11391
+ render.err("Usage: claudemesh apikey create <label> [--cap send,read] [--topic deploys]");
11392
+ return EXIT.INVALID_ARGS;
11393
+ }
11394
+ const caps = parseCapabilities(flags.cap);
11395
+ if (caps.length === 0) {
11396
+ render.err("at least one capability required: --cap send,read,state_write,admin");
11397
+ return EXIT.INVALID_ARGS;
11398
+ }
11399
+ const topicScopes = flags.topic ? flags.topic.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
11400
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11401
+ const result = await client.apiKeyCreate({
11402
+ label,
11403
+ capabilities: caps,
11404
+ topicScopes,
11405
+ expiresAt: flags.expires
11406
+ });
11407
+ if (!result) {
11408
+ render.err("apikey create failed");
11409
+ return EXIT.INTERNAL_ERROR;
11410
+ }
11411
+ if (flags.json) {
11412
+ console.log(JSON.stringify(result, null, 2));
11413
+ return EXIT.SUCCESS;
11414
+ }
11415
+ render.ok("created", `${bold(result.label)} ${dim(result.id.slice(0, 8))}`);
11416
+ process.stdout.write(`
11417
+ ${yellow("⚠ secret shown once — copy it now:")}
11418
+
11419
+ `);
11420
+ process.stdout.write(` ${green(result.secret)}
11421
+
11422
+ `);
11423
+ process.stdout.write(` ${dim(`capabilities: ${result.capabilities.join(", ")}`)}
11424
+ `);
11425
+ if (result.topicScopes?.length) {
11426
+ process.stdout.write(` ${dim(`topics: ${result.topicScopes.map((t) => "#" + t).join(", ")}`)}
11427
+ `);
11428
+ } else {
11429
+ process.stdout.write(` ${dim("topics: all (no scope)")}
11430
+ `);
11431
+ }
11432
+ return EXIT.SUCCESS;
11433
+ });
11434
+ }
11435
+ async function runApiKeyList(flags) {
11436
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11437
+ const keys = await client.apiKeyList();
11438
+ if (flags.json) {
11439
+ console.log(JSON.stringify(keys, null, 2));
11440
+ return EXIT.SUCCESS;
11441
+ }
11442
+ if (keys.length === 0) {
11443
+ render.info(dim("no api keys in this mesh."));
11444
+ return EXIT.SUCCESS;
11445
+ }
11446
+ render.section(`api keys (${keys.length})`);
11447
+ for (const k of keys) {
11448
+ const status = k.revokedAt ? red("revoked") : k.expiresAt && new Date(k.expiresAt) < new Date ? yellow("expired") : green("active");
11449
+ const lastUsed = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "never";
11450
+ const scope = k.topicScopes?.length ? k.topicScopes.map((t) => "#" + t).join(",") : "all topics";
11451
+ process.stdout.write(` ${bold(k.label)} ${status} ${dim(k.id.slice(0, 8))}
11452
+ `);
11453
+ process.stdout.write(` ${dim(`${k.prefix}… caps: ${k.capabilities.join(",")} scope: ${scope} last_used: ${lastUsed}`)}
11454
+ `);
11455
+ }
11456
+ return EXIT.SUCCESS;
11457
+ });
11458
+ }
11459
+ async function runApiKeyRevoke(id, flags) {
11460
+ if (!id) {
11461
+ render.err("Usage: claudemesh apikey revoke <id>");
11462
+ return EXIT.INVALID_ARGS;
11463
+ }
11464
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11465
+ const result = await client.apiKeyRevoke(id);
11466
+ if (!result.ok) {
11467
+ if (flags.json) {
11468
+ console.log(JSON.stringify({ ok: false, code: result.code, message: result.message }));
11469
+ } else {
11470
+ render.err(`${result.code}: ${result.message}`);
11471
+ }
11472
+ return result.code === "not_found" ? EXIT.NOT_FOUND : result.code === "not_unique" ? EXIT.INVALID_ARGS : EXIT.INTERNAL_ERROR;
11473
+ }
11474
+ if (flags.json)
11475
+ console.log(JSON.stringify({ revoked: result.id }));
11476
+ else
11477
+ render.ok("revoked", clay(result.id.slice(0, 8)));
11478
+ return EXIT.SUCCESS;
11479
+ });
11480
+ }
11481
+ var init_apikey = __esm(() => {
11482
+ init_connect();
11483
+ init_render();
11484
+ init_styles();
11485
+ init_exit_codes();
11486
+ });
11487
+
11488
+ // src/commands/topic.ts
11489
+ var exports_topic = {};
11490
+ __export(exports_topic, {
11491
+ runTopicMembers: () => runTopicMembers,
11492
+ runTopicMarkRead: () => runTopicMarkRead,
11493
+ runTopicList: () => runTopicList,
11494
+ runTopicLeave: () => runTopicLeave,
11495
+ runTopicJoin: () => runTopicJoin,
11496
+ runTopicHistory: () => runTopicHistory,
11497
+ runTopicCreate: () => runTopicCreate
11498
+ });
11499
+ async function runTopicCreate(name, flags) {
11500
+ if (!name) {
11501
+ render.err("Usage: claudemesh topic create <name> [--description X] [--visibility V]");
11502
+ return EXIT.INVALID_ARGS;
11503
+ }
11504
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11505
+ const result = await client.topicCreate({
11506
+ name,
11507
+ description: flags.description,
11508
+ visibility: flags.visibility
11509
+ });
11510
+ if (!result) {
11511
+ render.err("topic create failed");
11512
+ return EXIT.INTERNAL_ERROR;
11513
+ }
11514
+ if (flags.json) {
11515
+ console.log(JSON.stringify(result));
11516
+ return EXIT.SUCCESS;
11517
+ }
11518
+ if (result.created) {
11519
+ render.ok("created", `${clay("#" + name)} ${dim(result.id.slice(0, 8))}`);
11520
+ } else {
11521
+ render.info(dim(`already exists: #${name} ${result.id.slice(0, 8)}`));
11522
+ }
11523
+ return EXIT.SUCCESS;
11524
+ });
11525
+ }
11526
+ async function runTopicList(flags) {
11527
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11528
+ const topics = await client.topicList();
11529
+ if (flags.json) {
11530
+ console.log(JSON.stringify(topics, null, 2));
11531
+ return EXIT.SUCCESS;
11532
+ }
11533
+ if (topics.length === 0) {
11534
+ render.info(dim("no topics in this mesh."));
11535
+ return EXIT.SUCCESS;
11536
+ }
11537
+ render.section(`topics (${topics.length})`);
11538
+ for (const t of topics) {
11539
+ const vis = t.visibility === "public" ? green(t.visibility) : dim(t.visibility);
11540
+ process.stdout.write(` ${clay("#" + t.name)} ${vis} ${dim(`${t.memberCount} member${t.memberCount === 1 ? "" : "s"}`)}
11541
+ `);
11542
+ if (t.description)
11543
+ process.stdout.write(` ${dim(t.description)}
11544
+ `);
11545
+ }
11546
+ return EXIT.SUCCESS;
11547
+ });
11548
+ }
11549
+ async function runTopicJoin(topic, flags) {
11550
+ if (!topic) {
11551
+ render.err("Usage: claudemesh topic join <topic> [--role lead|member|observer]");
11552
+ return EXIT.INVALID_ARGS;
11553
+ }
11554
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11555
+ await client.topicJoin(topic, flags.role);
11556
+ if (flags.json)
11557
+ console.log(JSON.stringify({ joined: topic }));
11558
+ else
11559
+ render.ok("joined", clay("#" + topic));
11560
+ return EXIT.SUCCESS;
11561
+ });
11562
+ }
11563
+ async function runTopicLeave(topic, flags) {
11564
+ if (!topic) {
11565
+ render.err("Usage: claudemesh topic leave <topic>");
11566
+ return EXIT.INVALID_ARGS;
11567
+ }
11568
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11569
+ await client.topicLeave(topic);
11570
+ if (flags.json)
11571
+ console.log(JSON.stringify({ left: topic }));
11572
+ else
11573
+ render.ok("left", clay("#" + topic));
11574
+ return EXIT.SUCCESS;
11575
+ });
11576
+ }
11577
+ async function runTopicMembers(topic, flags) {
11578
+ if (!topic) {
11579
+ render.err("Usage: claudemesh topic members <topic>");
11580
+ return EXIT.INVALID_ARGS;
11581
+ }
11582
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11583
+ const members = await client.topicMembers(topic);
11584
+ if (flags.json) {
11585
+ console.log(JSON.stringify(members, null, 2));
11586
+ return EXIT.SUCCESS;
11587
+ }
11588
+ if (members.length === 0) {
11589
+ render.info(dim(`no members in ${clay("#" + topic)}.`));
11590
+ return EXIT.SUCCESS;
11591
+ }
11592
+ render.section(`${clay("#" + topic)} members (${members.length})`);
11593
+ for (const m of members) {
11594
+ process.stdout.write(` ${bold(m.displayName)} ${dim(m.role)} ${dim(m.pubkey.slice(0, 8))}
11595
+ `);
11596
+ }
11597
+ return EXIT.SUCCESS;
11598
+ });
11599
+ }
11600
+ async function runTopicHistory(topic, flags) {
11601
+ if (!topic) {
11602
+ render.err("Usage: claudemesh topic history <topic> [--limit N] [--before <id>]");
11603
+ return EXIT.INVALID_ARGS;
11604
+ }
11605
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11606
+ const limit = flags.limit ? Number(flags.limit) : undefined;
11607
+ const messages = await client.topicHistory({
11608
+ topic,
11609
+ limit,
11610
+ beforeId: flags.before
11611
+ });
11612
+ if (flags.json) {
11613
+ console.log(JSON.stringify(messages, null, 2));
11614
+ return EXIT.SUCCESS;
11615
+ }
11616
+ if (messages.length === 0) {
11617
+ render.info(dim(`no messages in ${clay("#" + topic)}.`));
11618
+ return EXIT.SUCCESS;
11619
+ }
11620
+ const ordered = [...messages].reverse();
11621
+ render.section(`${clay("#" + topic)} history (${ordered.length})`);
11622
+ for (const m of ordered) {
11623
+ const t = new Date(m.createdAt).toLocaleString();
11624
+ process.stdout.write(` ${dim(t)} ${bold(m.senderPubkey.slice(0, 8))} ${dim("(encrypted, " + m.ciphertext.length + "b)")}
11625
+ `);
11626
+ }
11627
+ return EXIT.SUCCESS;
11628
+ });
11629
+ }
11630
+ async function runTopicMarkRead(topic, flags) {
11631
+ if (!topic) {
11632
+ render.err("Usage: claudemesh topic read <topic>");
11633
+ return EXIT.INVALID_ARGS;
11634
+ }
11635
+ return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
11636
+ await client.topicMarkRead(topic);
11637
+ if (flags.json)
11638
+ console.log(JSON.stringify({ read: topic }));
11639
+ else
11640
+ render.ok("marked read", clay("#" + topic));
11641
+ return EXIT.SUCCESS;
11642
+ });
11643
+ }
11644
+ var init_topic = __esm(() => {
11645
+ init_connect();
11646
+ init_render();
11647
+ init_styles();
11648
+ init_exit_codes();
11649
+ });
11650
+
10381
11651
  // src/mcp/tools/definitions.ts
10382
11652
  var TOOLS;
10383
11653
  var init_definitions = __esm(() => {
@@ -10386,7 +11656,7 @@ var init_definitions = __esm(() => {
10386
11656
 
10387
11657
  // src/services/bridge/server.ts
10388
11658
  import { createServer as createServer2 } from "node:net";
10389
- import { mkdirSync as mkdirSync7, unlinkSync as unlinkSync2, existsSync as existsSync20, chmodSync as chmodSync5 } from "node:fs";
11659
+ import { mkdirSync as mkdirSync7, unlinkSync as unlinkSync2, existsSync as existsSync21, chmodSync as chmodSync5 } from "node:fs";
10390
11660
  async function resolveTarget2(client, to) {
10391
11661
  if (to.startsWith("@") || to === "*" || /^[0-9a-f]{64}$/i.test(to)) {
10392
11662
  return { ok: true, spec: to };
@@ -10500,10 +11770,10 @@ function handleConnection(socket, client) {
10500
11770
  function startBridgeServer(client) {
10501
11771
  const path = socketPath(client.meshSlug);
10502
11772
  const dir = socketDir();
10503
- if (!existsSync20(dir)) {
11773
+ if (!existsSync21(dir)) {
10504
11774
  mkdirSync7(dir, { recursive: true, mode: 448 });
10505
11775
  }
10506
- if (existsSync20(path)) {
11776
+ if (existsSync21(path)) {
10507
11777
  try {
10508
11778
  unlinkSync2(path);
10509
11779
  } catch {}
@@ -11553,6 +12823,21 @@ function classifyInvocation(command, positionals) {
11553
12823
  case "task": {
11554
12824
  return { resource: "task", verb: sub || "list", isWrite: isWrite(sub) };
11555
12825
  }
12826
+ case "topic": {
12827
+ const verb = sub || "list";
12828
+ const writeVerbs = new Set(["create", "join", "leave"]);
12829
+ return { resource: "topic", verb, isWrite: writeVerbs.has(verb) };
12830
+ }
12831
+ case "apikey":
12832
+ case "api-key": {
12833
+ const verb = sub || "list";
12834
+ const writeVerbs = new Set(["create", "revoke"]);
12835
+ return { resource: "apikey", verb, isWrite: writeVerbs.has(verb) };
12836
+ }
12837
+ case "bridge": {
12838
+ const verb = sub || "init";
12839
+ return { resource: "bridge", verb, isWrite: verb === "run" };
12840
+ }
11556
12841
  case "vector":
11557
12842
  case "graph":
11558
12843
  case "context":
@@ -11656,7 +12941,9 @@ var DEFAULT_POLICY = {
11656
12941
  { resource: "watch", verb: "remove", decision: "prompt", reason: "removes URL watcher" },
11657
12942
  { resource: "sql", verb: "execute", decision: "prompt", reason: "raw SQL write to mesh DB" },
11658
12943
  { resource: "graph", verb: "execute", decision: "prompt", reason: "graph mutation" },
11659
- { resource: "mesh", verb: "delete", decision: "prompt", reason: "deletes the mesh for everyone" }
12944
+ { resource: "mesh", verb: "delete", decision: "prompt", reason: "deletes the mesh for everyone" },
12945
+ { resource: "apikey", verb: "create", decision: "prompt", reason: "issues a long-lived credential" },
12946
+ { resource: "apikey", verb: "revoke", decision: "prompt", reason: "irreversibly disables a credential" }
11660
12947
  ]
11661
12948
  };
11662
12949
  var USER_POLICY_PATH = join(homedir(), ".claudemesh", "policy.yaml");
@@ -11915,6 +13202,25 @@ Profile / presence (resource form)
11915
13202
  claudemesh group join @<name> join a group (--role X)
11916
13203
  claudemesh group leave @<name> leave a group
11917
13204
 
13205
+ API keys (REST + external WS auth, v0.2.0)
13206
+ claudemesh apikey create <label> issue [--cap send,read] [--topic deploys]
13207
+ claudemesh apikey list show keys (status, last-used, scope)
13208
+ claudemesh apikey revoke <id> revoke a key
13209
+
13210
+ Bridge (forward a topic between two meshes, v0.2.0)
13211
+ claudemesh bridge init print config template
13212
+ claudemesh bridge run <config> run bridge as a long-lived process
13213
+
13214
+ Topic (conversation scope, v0.2.0)
13215
+ claudemesh topic create <name> create a topic [--description --visibility]
13216
+ claudemesh topic list list topics in the mesh
13217
+ claudemesh topic join <topic> subscribe (via name or id)
13218
+ claudemesh topic leave <topic> unsubscribe
13219
+ claudemesh topic members <t> list topic subscribers
13220
+ claudemesh topic history <t> fetch message history [--limit --before]
13221
+ claudemesh topic read <topic> mark all as read
13222
+ claudemesh send "#topic" "msg" send to a topic
13223
+
11918
13224
  Schedule (resource form)
11919
13225
  claudemesh schedule msg <m> one-shot or recurring (alias: remind)
11920
13226
  claudemesh schedule list list pending
@@ -12618,6 +13924,86 @@ async function main() {
12618
13924
  }
12619
13925
  break;
12620
13926
  }
13927
+ case "bridge": {
13928
+ const sub = positionals[0];
13929
+ if (sub === "run") {
13930
+ const { runBridge: runBridge2 } = await Promise.resolve().then(() => (init_bridge(), exports_bridge));
13931
+ process.exit(await runBridge2(positionals[1] ?? ""));
13932
+ } else if (sub === "init" || sub === "config") {
13933
+ const { bridgeConfigTemplate: bridgeConfigTemplate2 } = await Promise.resolve().then(() => (init_bridge(), exports_bridge));
13934
+ console.log(bridgeConfigTemplate2());
13935
+ process.exit(EXIT.SUCCESS);
13936
+ } else {
13937
+ console.error("Usage: claudemesh bridge <run <config.yaml> | init>");
13938
+ process.exit(EXIT.INVALID_ARGS);
13939
+ }
13940
+ break;
13941
+ }
13942
+ case "apikey":
13943
+ case "api-key": {
13944
+ const sub = positionals[0];
13945
+ const f = {
13946
+ mesh: flags.mesh,
13947
+ json: !!flags.json,
13948
+ cap: flags.cap,
13949
+ topic: flags.topic,
13950
+ expires: flags.expires
13951
+ };
13952
+ const arg = positionals[1] ?? "";
13953
+ if (sub === "create") {
13954
+ const { runApiKeyCreate: runApiKeyCreate2 } = await Promise.resolve().then(() => (init_apikey(), exports_apikey));
13955
+ process.exit(await runApiKeyCreate2(arg, f));
13956
+ } else if (sub === "list") {
13957
+ const { runApiKeyList: runApiKeyList2 } = await Promise.resolve().then(() => (init_apikey(), exports_apikey));
13958
+ process.exit(await runApiKeyList2(f));
13959
+ } else if (sub === "revoke") {
13960
+ const { runApiKeyRevoke: runApiKeyRevoke2 } = await Promise.resolve().then(() => (init_apikey(), exports_apikey));
13961
+ process.exit(await runApiKeyRevoke2(arg, f));
13962
+ } else {
13963
+ console.error("Usage: claudemesh apikey <create|list|revoke>");
13964
+ process.exit(EXIT.INVALID_ARGS);
13965
+ }
13966
+ break;
13967
+ }
13968
+ case "topic": {
13969
+ const sub = positionals[0];
13970
+ const f = {
13971
+ mesh: flags.mesh,
13972
+ json: !!flags.json,
13973
+ description: flags.description,
13974
+ visibility: flags.visibility,
13975
+ role: flags.role,
13976
+ limit: flags.limit,
13977
+ before: flags.before
13978
+ };
13979
+ const arg = positionals[1] ?? "";
13980
+ if (sub === "create") {
13981
+ const { runTopicCreate: runTopicCreate2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13982
+ process.exit(await runTopicCreate2(arg, f));
13983
+ } else if (sub === "list") {
13984
+ const { runTopicList: runTopicList2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13985
+ process.exit(await runTopicList2(f));
13986
+ } else if (sub === "join") {
13987
+ const { runTopicJoin: runTopicJoin2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13988
+ process.exit(await runTopicJoin2(arg, f));
13989
+ } else if (sub === "leave") {
13990
+ const { runTopicLeave: runTopicLeave2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13991
+ process.exit(await runTopicLeave2(arg, f));
13992
+ } else if (sub === "members") {
13993
+ const { runTopicMembers: runTopicMembers2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13994
+ process.exit(await runTopicMembers2(arg, f));
13995
+ } else if (sub === "history") {
13996
+ const { runTopicHistory: runTopicHistory2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
13997
+ process.exit(await runTopicHistory2(arg, f));
13998
+ } else if (sub === "read") {
13999
+ const { runTopicMarkRead: runTopicMarkRead2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
14000
+ process.exit(await runTopicMarkRead2(arg, f));
14001
+ } else {
14002
+ console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read>");
14003
+ process.exit(EXIT.INVALID_ARGS);
14004
+ }
14005
+ break;
14006
+ }
12621
14007
  case "task": {
12622
14008
  const sub = positionals[0];
12623
14009
  const f = { mesh: flags.mesh, json: !!flags.json };
@@ -12666,4 +14052,4 @@ main().catch((err) => {
12666
14052
  process.exit(EXIT.INTERNAL_ERROR);
12667
14053
  });
12668
14054
 
12669
- //# debugId=C75596237B999F1364756E2164756E21
14055
+ //# debugId=131DC57DB8537D5464756E2164756E21