claudemesh-cli 0.6.1 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +490 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39930,6 +39930,81 @@ var require_libsodium_wrappers = __commonJS((exports) => {
39930
39930
  })(exports);
39931
39931
  });
39932
39932
 
39933
+ // src/crypto/keypair.ts
39934
+ var exports_keypair = {};
39935
+ __export(exports_keypair, {
39936
+ generateKeypair: () => generateKeypair2,
39937
+ ensureSodium: () => ensureSodium
39938
+ });
39939
+ async function ensureSodium() {
39940
+ if (!ready) {
39941
+ await import_libsodium_wrappers.default.ready;
39942
+ ready = true;
39943
+ }
39944
+ return import_libsodium_wrappers.default;
39945
+ }
39946
+ async function generateKeypair2() {
39947
+ const s = await ensureSodium();
39948
+ const kp = s.crypto_sign_keypair();
39949
+ return {
39950
+ publicKey: s.to_hex(kp.publicKey),
39951
+ secretKey: s.to_hex(kp.privateKey)
39952
+ };
39953
+ }
39954
+ var import_libsodium_wrappers, ready = false;
39955
+ var init_keypair = __esm(() => {
39956
+ import_libsodium_wrappers = __toESM(require_libsodium_wrappers(), 1);
39957
+ });
39958
+
39959
+ // src/crypto/file-crypto.ts
39960
+ var exports_file_crypto = {};
39961
+ __export(exports_file_crypto, {
39962
+ sealKeyForPeer: () => sealKeyForPeer,
39963
+ openSealedKey: () => openSealedKey,
39964
+ encryptFile: () => encryptFile,
39965
+ decryptFile: () => decryptFile
39966
+ });
39967
+ async function encryptFile(plaintext) {
39968
+ const sodium2 = await ensureSodium();
39969
+ const key = sodium2.randombytes_buf(sodium2.crypto_secretbox_KEYBYTES);
39970
+ const nonce = sodium2.randombytes_buf(sodium2.crypto_secretbox_NONCEBYTES);
39971
+ const ciphertext = sodium2.crypto_secretbox_easy(plaintext, nonce, key);
39972
+ return {
39973
+ ciphertext,
39974
+ nonce: sodium2.to_base64(nonce, sodium2.base64_variants.ORIGINAL),
39975
+ key
39976
+ };
39977
+ }
39978
+ async function decryptFile(ciphertext, nonceB64, key) {
39979
+ const sodium2 = await ensureSodium();
39980
+ try {
39981
+ const nonce = sodium2.from_base64(nonceB64, sodium2.base64_variants.ORIGINAL);
39982
+ return sodium2.crypto_secretbox_open_easy(ciphertext, nonce, key);
39983
+ } catch {
39984
+ return null;
39985
+ }
39986
+ }
39987
+ async function sealKeyForPeer(kf, recipientPubkeyHex) {
39988
+ const sodium2 = await ensureSodium();
39989
+ const recipientCurve = sodium2.crypto_sign_ed25519_pk_to_curve25519(sodium2.from_hex(recipientPubkeyHex));
39990
+ const sealed = sodium2.crypto_box_seal(kf, recipientCurve);
39991
+ return sodium2.to_base64(sealed, sodium2.base64_variants.ORIGINAL);
39992
+ }
39993
+ async function openSealedKey(sealedB64, myPubkeyHex, mySecretKeyHex) {
39994
+ const sodium2 = await ensureSodium();
39995
+ try {
39996
+ const myCurvePub = sodium2.crypto_sign_ed25519_pk_to_curve25519(sodium2.from_hex(myPubkeyHex));
39997
+ const myCurveSec = sodium2.crypto_sign_ed25519_sk_to_curve25519(sodium2.from_hex(mySecretKeyHex));
39998
+ const sealed = sodium2.from_base64(sealedB64, sodium2.base64_variants.ORIGINAL);
39999
+ return sodium2.crypto_box_seal_open(sealed, myCurvePub, myCurveSec);
40000
+ } catch {
40001
+ return null;
40002
+ }
40003
+ }
40004
+ var init_file_crypto = __esm(() => {
40005
+ init_keypair();
40006
+ });
40007
+
39933
40008
  // ../../node_modules/citty/dist/_chunks/libs/scule.mjs
39934
40009
  var NUMBER_CHAR_RE = /\d/;
39935
40010
  var STR_SPLITTERS = [
@@ -46963,7 +47038,7 @@ var TOOLS = [
46963
47038
  },
46964
47039
  {
46965
47040
  name: "share_file",
46966
- description: "Share a persistent file with the mesh. All current and future peers can access it.",
47041
+ description: "Share a persistent file with the mesh. All current and future peers can access it. If `to` is specified, the file is E2E encrypted and only accessible to that peer (and you).",
46967
47042
  inputSchema: {
46968
47043
  type: "object",
46969
47044
  properties: {
@@ -46976,6 +47051,10 @@ var TOOLS = [
46976
47051
  type: "array",
46977
47052
  items: { type: "string" },
46978
47053
  description: "Tags for categorization"
47054
+ },
47055
+ to: {
47056
+ type: "string",
47057
+ description: "Peer display name or pubkey hex — if set, file is E2E encrypted for this peer only"
46979
47058
  }
46980
47059
  },
46981
47060
  required: ["path"]
@@ -47029,6 +47108,18 @@ var TOOLS = [
47029
47108
  required: ["id"]
47030
47109
  }
47031
47110
  },
47111
+ {
47112
+ name: "grant_file_access",
47113
+ description: "Grant a peer access to an E2E encrypted file you shared. You must be the owner.",
47114
+ inputSchema: {
47115
+ type: "object",
47116
+ properties: {
47117
+ fileId: { type: "string", description: "File ID" },
47118
+ to: { type: "string", description: "Peer display name or pubkey hex to grant access to" }
47119
+ },
47120
+ required: ["fileId", "to"]
47121
+ }
47122
+ },
47032
47123
  {
47033
47124
  name: "vector_store",
47034
47125
  description: "Store an embedding in a per-mesh Qdrant collection. Auto-creates the collection on first use.",
@@ -47321,26 +47412,8 @@ var wrapper_default = import_websocket.default;
47321
47412
  // src/ws/client.ts
47322
47413
  import { randomBytes } from "node:crypto";
47323
47414
 
47324
- // src/crypto/keypair.ts
47325
- var import_libsodium_wrappers = __toESM(require_libsodium_wrappers(), 1);
47326
- var ready = false;
47327
- async function ensureSodium() {
47328
- if (!ready) {
47329
- await import_libsodium_wrappers.default.ready;
47330
- ready = true;
47331
- }
47332
- return import_libsodium_wrappers.default;
47333
- }
47334
- async function generateKeypair2() {
47335
- const s = await ensureSodium();
47336
- const kp = s.crypto_sign_keypair();
47337
- return {
47338
- publicKey: s.to_hex(kp.publicKey),
47339
- secretKey: s.to_hex(kp.privateKey)
47340
- };
47341
- }
47342
-
47343
47415
  // src/crypto/envelope.ts
47416
+ init_keypair();
47344
47417
  var HEX_PUBKEY = /^[0-9a-f]{64}$/;
47345
47418
  function isDirectTarget(targetSpec) {
47346
47419
  return HEX_PUBKEY.test(targetSpec);
@@ -47371,6 +47444,7 @@ async function decryptDirect(envelope, senderPubkeyHex, recipientSecretKeyHex) {
47371
47444
  }
47372
47445
 
47373
47446
  // src/crypto/hello-sig.ts
47447
+ init_keypair();
47374
47448
  async function signHello(meshId, memberId, pubkey, secretKeyHex) {
47375
47449
  const s = await ensureSodium();
47376
47450
  const timestamp = Date.now();
@@ -47380,6 +47454,7 @@ async function signHello(meshId, memberId, pubkey, secretKeyHex) {
47380
47454
  }
47381
47455
 
47382
47456
  // src/ws/client.ts
47457
+ init_keypair();
47383
47458
  var MAX_QUEUED = 100;
47384
47459
  var HELLO_ACK_TIMEOUT_MS = 5000;
47385
47460
  var BACKOFF_CAPS = [1000, 2000, 4000, 8000, 16000, 30000];
@@ -47401,6 +47476,7 @@ class BrokerClient {
47401
47476
  stateChangeHandlers = new Set;
47402
47477
  sessionPubkey = null;
47403
47478
  sessionSecretKey = null;
47479
+ grantFileAccessResolvers = [];
47404
47480
  closed = false;
47405
47481
  reconnectAttempt = 0;
47406
47482
  helloTimer = null;
@@ -47421,6 +47497,12 @@ class BrokerClient {
47421
47497
  get pushHistory() {
47422
47498
  return this.pushBuffer;
47423
47499
  }
47500
+ getSessionPubkey() {
47501
+ return this.sessionPubkey;
47502
+ }
47503
+ getSessionSecretKey() {
47504
+ return this.sessionSecretKey;
47505
+ }
47424
47506
  async connect() {
47425
47507
  if (this.closed)
47426
47508
  throw new Error("client is closed");
@@ -47767,7 +47849,10 @@ class BrokerClient {
47767
47849
  "X-File-Name": fileName,
47768
47850
  "X-Tags": JSON.stringify(opts.tags ?? []),
47769
47851
  "X-Persistent": String(opts.persistent ?? true),
47770
- "X-Target-Spec": opts.targetSpec ?? ""
47852
+ "X-Target-Spec": opts.targetSpec ?? "",
47853
+ ...opts.encrypted ? { "X-Encrypted": "true" } : {},
47854
+ ...opts.ownerPubkey ? { "X-Owner-Pubkey": opts.ownerPubkey } : {},
47855
+ ...opts.fileKeys?.length ? { "X-File-Keys": JSON.stringify(opts.fileKeys) } : {}
47771
47856
  },
47772
47857
  body: data,
47773
47858
  signal: AbortSignal.timeout(30000)
@@ -47778,6 +47863,22 @@ class BrokerClient {
47778
47863
  }
47779
47864
  return body.fileId;
47780
47865
  }
47866
+ async grantFileAccess(fileId, peerPubkey, sealedKey) {
47867
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47868
+ return false;
47869
+ return new Promise((resolve) => {
47870
+ const resolvers = this.grantFileAccessResolvers;
47871
+ resolvers.push(resolve);
47872
+ this.ws.send(JSON.stringify({ type: "grant_file_access", fileId, peerPubkey, sealedKey }));
47873
+ setTimeout(() => {
47874
+ const idx = resolvers.indexOf(resolve);
47875
+ if (idx !== -1) {
47876
+ resolvers.splice(idx, 1);
47877
+ resolve(false);
47878
+ }
47879
+ }, 5000);
47880
+ });
47881
+ }
47781
47882
  async vectorStore(collection, text, metadata) {
47782
47883
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47783
47884
  return null;
@@ -48178,7 +48279,12 @@ class BrokerClient {
48178
48279
  const resolver = this.fileUrlResolvers.shift();
48179
48280
  if (resolver) {
48180
48281
  if (msg.url) {
48181
- resolver({ url: String(msg.url), name: String(msg.name ?? "") });
48282
+ resolver({
48283
+ url: String(msg.url),
48284
+ name: String(msg.name ?? ""),
48285
+ encrypted: msg.encrypted ? true : undefined,
48286
+ sealedKey: msg.sealedKey ? String(msg.sealedKey) : undefined
48287
+ });
48182
48288
  } else {
48183
48289
  resolver(null);
48184
48290
  }
@@ -48199,6 +48305,12 @@ class BrokerClient {
48199
48305
  resolver(accesses);
48200
48306
  return;
48201
48307
  }
48308
+ if (msg.type === "grant_file_access_ok") {
48309
+ const resolver = this.grantFileAccessResolvers.shift();
48310
+ if (resolver)
48311
+ resolver(true);
48312
+ return;
48313
+ }
48202
48314
  if (msg.type === "vector_stored") {
48203
48315
  const resolver = this.vectorStoredResolvers.shift();
48204
48316
  if (resolver)
@@ -48795,7 +48907,7 @@ ${lines.join(`
48795
48907
  return text(`Forgotten: ${id}`);
48796
48908
  }
48797
48909
  case "share_file": {
48798
- const { path: filePath, name: fileName, tags } = args ?? {};
48910
+ const { path: filePath, name: fileName, tags, to: fileTo } = args ?? {};
48799
48911
  if (!filePath)
48800
48912
  return text("share_file: `path` required", true);
48801
48913
  const { existsSync: existsSync2 } = await import("node:fs");
@@ -48804,6 +48916,56 @@ ${lines.join(`
48804
48916
  const client = allClients()[0];
48805
48917
  if (!client)
48806
48918
  return text("share_file: not connected", true);
48919
+ if (fileTo) {
48920
+ const { encryptFile: encryptFile2, sealKeyForPeer: sealKeyForPeer2 } = await Promise.resolve().then(() => (init_file_crypto(), exports_file_crypto));
48921
+ const { readFileSync: readFileSync2, writeFileSync: writeFileSync2, mkdtempSync, unlinkSync, rmdirSync } = await import("node:fs");
48922
+ const { tmpdir } = await import("node:os");
48923
+ const { join: join2, basename } = await import("node:path");
48924
+ const peers = await client.listPeers();
48925
+ const targetPeer = peers.find((p) => p.pubkey === fileTo || p.displayName === fileTo);
48926
+ if (!targetPeer) {
48927
+ return text(`share_file: peer not found: ${fileTo}`, true);
48928
+ }
48929
+ const plaintext = readFileSync2(filePath);
48930
+ const { ciphertext, nonce, key } = await encryptFile2(new Uint8Array(plaintext));
48931
+ const sealedForTarget = await sealKeyForPeer2(key, targetPeer.pubkey);
48932
+ const myPubkey = client.getSessionPubkey();
48933
+ const sealedForSelf = myPubkey ? await sealKeyForPeer2(key, myPubkey) : null;
48934
+ const fileKeys = [
48935
+ { peerPubkey: targetPeer.pubkey, sealedKey: sealedForTarget },
48936
+ ...sealedForSelf && myPubkey ? [{ peerPubkey: myPubkey, sealedKey: sealedForSelf }] : []
48937
+ ];
48938
+ const { ensureSodium: ensureSodium2 } = await Promise.resolve().then(() => (init_keypair(), exports_keypair));
48939
+ const sodium2 = await ensureSodium2();
48940
+ const nonceBytes = sodium2.from_base64(nonce, sodium2.base64_variants.ORIGINAL);
48941
+ const combined = new Uint8Array(nonceBytes.length + ciphertext.length);
48942
+ combined.set(nonceBytes, 0);
48943
+ combined.set(ciphertext, nonceBytes.length);
48944
+ const baseName = fileName ?? basename(filePath);
48945
+ const tmpDir = mkdtempSync(join2(tmpdir(), "cm-"));
48946
+ const tmpPath = join2(tmpDir, baseName);
48947
+ writeFileSync2(tmpPath, combined);
48948
+ try {
48949
+ const fileId = await client.uploadFile(tmpPath, client.meshId, client.meshSlug, {
48950
+ name: baseName,
48951
+ tags,
48952
+ persistent: true,
48953
+ encrypted: true,
48954
+ ownerPubkey: myPubkey ?? undefined,
48955
+ fileKeys
48956
+ });
48957
+ return text(`Shared (E2E encrypted): ${baseName} → ${targetPeer.displayName} (${fileId})`);
48958
+ } catch (e) {
48959
+ return text(`share_file: upload failed — ${e instanceof Error ? e.message : String(e)}`, true);
48960
+ } finally {
48961
+ try {
48962
+ unlinkSync(tmpPath);
48963
+ } catch {}
48964
+ try {
48965
+ rmdirSync(tmpDir);
48966
+ } catch {}
48967
+ }
48968
+ }
48807
48969
  try {
48808
48970
  const fileId = await client.uploadFile(filePath, client.meshId, client.meshSlug, {
48809
48971
  name: fileName,
@@ -48825,6 +48987,34 @@ ${lines.join(`
48825
48987
  const result = await client.getFile(id);
48826
48988
  if (!result)
48827
48989
  return text(`get_file: file ${id} not found`, true);
48990
+ if (result.encrypted && result.sealedKey) {
48991
+ const { openSealedKey: openSealedKey2, decryptFile: decryptFile2 } = await Promise.resolve().then(() => (init_file_crypto(), exports_file_crypto));
48992
+ const { ensureSodium: ensureSodium2 } = await Promise.resolve().then(() => (init_keypair(), exports_keypair));
48993
+ const myPubkey = client.getSessionPubkey();
48994
+ const mySecret = client.getSessionSecretKey();
48995
+ if (!myPubkey || !mySecret) {
48996
+ return text("get_file: no session keypair — cannot decrypt", true);
48997
+ }
48998
+ const kf = await openSealedKey2(result.sealedKey, myPubkey, mySecret);
48999
+ if (!kf)
49000
+ return text("get_file: failed to open sealed key", true);
49001
+ const resp = await fetch(result.url, { signal: AbortSignal.timeout(30000) });
49002
+ if (!resp.ok)
49003
+ return text(`get_file: download failed (${resp.status})`, true);
49004
+ const buf = new Uint8Array(await resp.arrayBuffer());
49005
+ const sodium2 = await ensureSodium2();
49006
+ const NONCE_BYTES = sodium2.crypto_secretbox_NONCEBYTES;
49007
+ const nonce = sodium2.to_base64(buf.slice(0, NONCE_BYTES), sodium2.base64_variants.ORIGINAL);
49008
+ const ciphertext = buf.slice(NONCE_BYTES);
49009
+ const plaintext = await decryptFile2(ciphertext, nonce, kf);
49010
+ if (!plaintext)
49011
+ return text("get_file: decryption failed", true);
49012
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("node:fs");
49013
+ const { dirname: dirname3 } = await import("node:path");
49014
+ mkdirSync3(dirname3(save_to), { recursive: true });
49015
+ writeFileSync3(save_to, plaintext);
49016
+ return text(`Downloaded and decrypted: ${result.name} → ${save_to}`);
49017
+ }
48828
49018
  const res = await fetch(result.url, { signal: AbortSignal.timeout(30000) });
48829
49019
  if (!res.ok)
48830
49020
  return text(`get_file: download failed (${res.status})`, true);
@@ -49167,6 +49357,38 @@ ${rows.join(`
49167
49357
  return text(results.join(`
49168
49358
  `));
49169
49359
  }
49360
+ case "grant_file_access": {
49361
+ const { fileId, to: grantTo } = args ?? {};
49362
+ if (!fileId || !grantTo)
49363
+ return text("grant_file_access: `fileId` and `to` required", true);
49364
+ const client = allClients()[0];
49365
+ if (!client)
49366
+ return text("grant_file_access: not connected", true);
49367
+ const peers = await client.listPeers();
49368
+ const targetPeer = peers.find((p) => p.pubkey === grantTo || p.displayName === grantTo);
49369
+ if (!targetPeer)
49370
+ return text(`grant_file_access: peer not found: ${grantTo}`, true);
49371
+ const result = await client.getFile(fileId);
49372
+ if (!result)
49373
+ return text("grant_file_access: file not found", true);
49374
+ if (!result.encrypted)
49375
+ return text("grant_file_access: file is not encrypted", true);
49376
+ if (!result.sealedKey)
49377
+ return text("grant_file_access: no key available (are you the owner?)", true);
49378
+ const { openSealedKey: openSealedKey2, sealKeyForPeer: sealKeyForPeer2 } = await Promise.resolve().then(() => (init_file_crypto(), exports_file_crypto));
49379
+ const myPubkey = client.getSessionPubkey();
49380
+ const mySecret = client.getSessionSecretKey();
49381
+ if (!myPubkey || !mySecret)
49382
+ return text("grant_file_access: no session keypair", true);
49383
+ const kf = await openSealedKey2(result.sealedKey, myPubkey, mySecret);
49384
+ if (!kf)
49385
+ return text("grant_file_access: cannot decrypt your own key", true);
49386
+ const sealedForPeer = await sealKeyForPeer2(kf, targetPeer.pubkey);
49387
+ const ok = await client.grantFileAccess(fileId, targetPeer.pubkey, sealedForPeer);
49388
+ if (!ok)
49389
+ return text("grant_file_access: broker did not confirm", true);
49390
+ return text(`Access granted: ${targetPeer.displayName} can now download file ${fileId}`);
49391
+ }
49170
49392
  default:
49171
49393
  return text(`Unknown tool: ${name}`, true);
49172
49394
  }
@@ -49613,6 +49835,7 @@ function runUninstall() {
49613
49835
  }
49614
49836
 
49615
49837
  // src/invite/parse.ts
49838
+ init_keypair();
49616
49839
  function validatePayload(obj) {
49617
49840
  if (!obj || typeof obj !== "object")
49618
49841
  throw new Error("invite payload is not an object");
@@ -49733,6 +49956,7 @@ async function enrollWithBroker2(args) {
49733
49956
  }
49734
49957
 
49735
49958
  // src/commands/join.ts
49959
+ init_keypair();
49736
49960
  init_config();
49737
49961
  init_env();
49738
49962
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
@@ -50232,7 +50456,7 @@ init_config();
50232
50456
  // package.json
50233
50457
  var package_default = {
50234
50458
  name: "claudemesh-cli",
50235
- version: "0.6.1",
50459
+ version: "0.6.3",
50236
50460
  description: "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
50237
50461
  keywords: [
50238
50462
  "claude-code",
@@ -50640,6 +50864,185 @@ function runWelcome() {
50640
50864
  console.log("");
50641
50865
  }
50642
50866
 
50867
+ // src/commands/connect.ts
50868
+ import { hostname as hostname3 } from "node:os";
50869
+ init_config();
50870
+ async function withMesh(opts, fn) {
50871
+ const config2 = loadConfig();
50872
+ if (config2.meshes.length === 0) {
50873
+ console.error("No meshes joined. Run `claudemesh join <url>` first.");
50874
+ process.exit(1);
50875
+ }
50876
+ let mesh;
50877
+ if (opts.meshSlug) {
50878
+ const found = config2.meshes.find((m) => m.slug === opts.meshSlug);
50879
+ if (!found) {
50880
+ console.error(`Mesh "${opts.meshSlug}" not found. Joined: ${config2.meshes.map((m) => m.slug).join(", ")}`);
50881
+ process.exit(1);
50882
+ }
50883
+ mesh = found;
50884
+ } else if (config2.meshes.length === 1) {
50885
+ mesh = config2.meshes[0];
50886
+ } else {
50887
+ console.error(`Multiple meshes joined. Specify one with --mesh <slug>.
50888
+ Joined: ${config2.meshes.map((m) => m.slug).join(", ")}`);
50889
+ process.exit(1);
50890
+ }
50891
+ const displayName = opts.displayName ?? config2.displayName ?? `${hostname3()}-${process.pid}`;
50892
+ const client = new BrokerClient(mesh, { displayName });
50893
+ try {
50894
+ await client.connect();
50895
+ const result = await fn(client, mesh);
50896
+ return result;
50897
+ } finally {
50898
+ client.close();
50899
+ }
50900
+ }
50901
+
50902
+ // src/commands/peers.ts
50903
+ async function runPeers(flags) {
50904
+ const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
50905
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
50906
+ const bold2 = (s) => useColor ? `\x1B[1m${s}\x1B[22m` : s;
50907
+ const green = (s) => useColor ? `\x1B[32m${s}\x1B[39m` : s;
50908
+ const yellow = (s) => useColor ? `\x1B[33m${s}\x1B[39m` : s;
50909
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client, mesh) => {
50910
+ const peers = await client.listPeers();
50911
+ if (flags.json) {
50912
+ console.log(JSON.stringify(peers, null, 2));
50913
+ return;
50914
+ }
50915
+ if (peers.length === 0) {
50916
+ console.log(dim(`No peers connected on mesh "${mesh.slug}".`));
50917
+ return;
50918
+ }
50919
+ console.log(bold2(`Peers on ${mesh.slug}`) + dim(` (${peers.length})`));
50920
+ console.log("");
50921
+ for (const p of peers) {
50922
+ const groups = p.groups.length ? " [" + p.groups.map((g) => `@${g.name}${g.role ? `:${g.role}` : ""}`).join(", ") + "]" : "";
50923
+ const statusIcon = p.status === "working" ? yellow("●") : green("●");
50924
+ const name = bold2(p.displayName);
50925
+ const summary = p.summary ? dim(` ${p.summary}`) : "";
50926
+ console.log(` ${statusIcon} ${name}${groups}${summary}`);
50927
+ }
50928
+ console.log("");
50929
+ });
50930
+ }
50931
+
50932
+ // src/commands/send.ts
50933
+ async function runSend(flags, to, message) {
50934
+ const priority = flags.priority === "now" ? "now" : flags.priority === "low" ? "low" : "next";
50935
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
50936
+ let targetSpec = to;
50937
+ if (!to.startsWith("@") && to !== "*" && !/^[0-9a-f]{64}$/i.test(to)) {
50938
+ const peers = await client.listPeers();
50939
+ const match = peers.find((p) => p.displayName.toLowerCase() === to.toLowerCase());
50940
+ if (!match) {
50941
+ const names = peers.map((p) => p.displayName).join(", ");
50942
+ console.error(`Peer "${to}" not found. Online: ${names || "(none)"}`);
50943
+ process.exit(1);
50944
+ }
50945
+ targetSpec = match.pubkey;
50946
+ }
50947
+ const result = await client.send(targetSpec, message, priority);
50948
+ if (result.ok) {
50949
+ console.log(`✓ Sent to ${to}${result.messageId ? ` (${result.messageId.slice(0, 8)})` : ""}`);
50950
+ } else {
50951
+ console.error(`✗ Send failed: ${result.error ?? "unknown error"}`);
50952
+ process.exit(1);
50953
+ }
50954
+ });
50955
+ }
50956
+
50957
+ // src/commands/inbox.ts
50958
+ function formatMessage(msg, useColor) {
50959
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
50960
+ const bold2 = (s) => useColor ? `\x1B[1m${s}\x1B[22m` : s;
50961
+ const text2 = msg.plaintext ?? `[encrypted: ${msg.ciphertext.slice(0, 32)}…]`;
50962
+ const from = msg.senderPubkey.slice(0, 8);
50963
+ const time3 = new Date(msg.createdAt).toLocaleTimeString();
50964
+ const kindTag = msg.kind === "direct" ? "→ direct" : msg.kind;
50965
+ return ` ${bold2(from)} ${dim(`[${kindTag}] ${time3}`)}
50966
+ ${text2}`;
50967
+ }
50968
+ async function runInbox(flags) {
50969
+ const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
50970
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
50971
+ const bold2 = (s) => useColor ? `\x1B[1m${s}\x1B[22m` : s;
50972
+ const waitMs = (flags.wait ?? 1) * 1000;
50973
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client, mesh) => {
50974
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
50975
+ const messages = client.drainPushBuffer();
50976
+ if (flags.json) {
50977
+ console.log(JSON.stringify(messages, null, 2));
50978
+ return;
50979
+ }
50980
+ if (messages.length === 0) {
50981
+ console.log(dim(`No messages on mesh "${mesh.slug}".`));
50982
+ return;
50983
+ }
50984
+ console.log(bold2(`Inbox — ${mesh.slug}`) + dim(` (${messages.length} message${messages.length === 1 ? "" : "s"})`));
50985
+ console.log("");
50986
+ for (const msg of messages) {
50987
+ console.log(formatMessage(msg, useColor));
50988
+ console.log("");
50989
+ }
50990
+ });
50991
+ }
50992
+
50993
+ // src/commands/state.ts
50994
+ async function runStateGet(flags, key) {
50995
+ const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
50996
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
50997
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
50998
+ const entry = await client.getState(key);
50999
+ if (!entry) {
51000
+ console.log(dim(`(not set)`));
51001
+ return;
51002
+ }
51003
+ if (flags.json) {
51004
+ console.log(JSON.stringify(entry, null, 2));
51005
+ return;
51006
+ }
51007
+ const val = typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value);
51008
+ console.log(val);
51009
+ console.log(dim(` set by ${entry.updatedBy} at ${new Date(entry.updatedAt).toLocaleString()}`));
51010
+ });
51011
+ }
51012
+ async function runStateSet(flags, key, value) {
51013
+ let parsed;
51014
+ try {
51015
+ parsed = JSON.parse(value);
51016
+ } catch {
51017
+ parsed = value;
51018
+ }
51019
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
51020
+ await client.setState(key, parsed);
51021
+ console.log(`✓ ${key} = ${JSON.stringify(parsed)}`);
51022
+ });
51023
+ }
51024
+ async function runStateList(flags) {
51025
+ const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
51026
+ const dim = (s) => useColor ? `\x1B[2m${s}\x1B[22m` : s;
51027
+ const bold2 = (s) => useColor ? `\x1B[1m${s}\x1B[22m` : s;
51028
+ await withMesh({ meshSlug: flags.mesh ?? null }, async (client, mesh) => {
51029
+ const entries = await client.listState();
51030
+ if (flags.json) {
51031
+ console.log(JSON.stringify(entries, null, 2));
51032
+ return;
51033
+ }
51034
+ if (entries.length === 0) {
51035
+ console.log(dim(`No state on mesh "${mesh.slug}".`));
51036
+ return;
51037
+ }
51038
+ for (const e of entries) {
51039
+ const val = typeof e.value === "string" ? e.value : JSON.stringify(e.value);
51040
+ console.log(`${bold2(e.key)}: ${val}`);
51041
+ console.log(dim(` ${e.updatedBy} · ${new Date(e.updatedAt).toLocaleString()}`));
51042
+ }
51043
+ });
51044
+ }
51045
+
50643
51046
  // src/index.ts
50644
51047
  var launch = defineCommand({
50645
51048
  meta: {
@@ -50762,6 +51165,69 @@ var main = defineCommand({
50762
51165
  }
50763
51166
  }),
50764
51167
  leave,
51168
+ peers: defineCommand({
51169
+ meta: { name: "peers", description: "List connected peers in the mesh" },
51170
+ args: {
51171
+ mesh: { type: "string", description: "Mesh slug (auto-selected if only one joined)" },
51172
+ json: { type: "boolean", description: "Output as JSON", default: false }
51173
+ },
51174
+ async run({ args }) {
51175
+ await runPeers(args);
51176
+ }
51177
+ }),
51178
+ send: defineCommand({
51179
+ meta: { name: "send", description: "Send a message to a peer, group, or broadcast" },
51180
+ args: {
51181
+ to: { type: "positional", description: "Recipient: display name, @group, pubkey, or *", required: true },
51182
+ message: { type: "positional", description: "Message text", required: true },
51183
+ mesh: { type: "string", description: "Mesh slug (auto-selected if only one joined)" },
51184
+ priority: { type: "string", description: "now | next (default) | low" }
51185
+ },
51186
+ async run({ args }) {
51187
+ await runSend(args, args.to, args.message);
51188
+ }
51189
+ }),
51190
+ inbox: defineCommand({
51191
+ meta: { name: "inbox", description: "Read pending peer messages" },
51192
+ args: {
51193
+ mesh: { type: "string", description: "Mesh slug (auto-selected if only one joined)" },
51194
+ json: { type: "boolean", description: "Output as JSON", default: false },
51195
+ wait: { type: "string", description: "Seconds to wait for broker delivery (default: 1)" }
51196
+ },
51197
+ async run({ args }) {
51198
+ await runInbox({ ...args, wait: args.wait ? parseInt(args.wait, 10) : undefined });
51199
+ }
51200
+ }),
51201
+ state: defineCommand({
51202
+ meta: { name: "state", description: "Read or write shared mesh state" },
51203
+ args: {
51204
+ action: { type: "positional", description: "get | set | list", required: true },
51205
+ key: { type: "positional", description: "State key (required for get/set)" },
51206
+ value: { type: "positional", description: "Value to set (required for set)" },
51207
+ mesh: { type: "string", description: "Mesh slug (auto-selected if only one joined)" },
51208
+ json: { type: "boolean", description: "Output as JSON", default: false }
51209
+ },
51210
+ async run({ args }) {
51211
+ if (args.action === "list") {
51212
+ await runStateList(args);
51213
+ } else if (args.action === "get") {
51214
+ if (!args.key) {
51215
+ console.error("Usage: claudemesh state get <key>");
51216
+ process.exit(1);
51217
+ }
51218
+ await runStateGet(args, args.key);
51219
+ } else if (args.action === "set") {
51220
+ if (!args.key || !args.value) {
51221
+ console.error("Usage: claudemesh state set <key> <value>");
51222
+ process.exit(1);
51223
+ }
51224
+ await runStateSet(args, args.key, args.value);
51225
+ } else {
51226
+ console.error(`Unknown action "${args.action}". Use: get, set, list`);
51227
+ process.exit(1);
51228
+ }
51229
+ }
51230
+ }),
50765
51231
  status: defineCommand({
50766
51232
  meta: { name: "status", description: "Check broker reachability for each joined mesh" },
50767
51233
  async run() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemesh-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
5
5
  "keywords": [
6
6
  "claude-code",