claudemesh-cli 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +586 -30
  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
  }
@@ -49399,6 +49621,73 @@ function writeClaudeSettings(obj) {
49399
49621
  writeFileSync2(CLAUDE_SETTINGS, JSON.stringify(obj, null, 2) + `
49400
49622
  `, "utf-8");
49401
49623
  }
49624
+ var CLAUDEMESH_TOOLS = [
49625
+ "mcp__claudemesh__send_message",
49626
+ "mcp__claudemesh__list_peers",
49627
+ "mcp__claudemesh__check_messages",
49628
+ "mcp__claudemesh__set_summary",
49629
+ "mcp__claudemesh__set_status",
49630
+ "mcp__claudemesh__join_group",
49631
+ "mcp__claudemesh__leave_group",
49632
+ "mcp__claudemesh__get_state",
49633
+ "mcp__claudemesh__set_state",
49634
+ "mcp__claudemesh__list_state",
49635
+ "mcp__claudemesh__remember",
49636
+ "mcp__claudemesh__recall",
49637
+ "mcp__claudemesh__forget",
49638
+ "mcp__claudemesh__share_file",
49639
+ "mcp__claudemesh__get_file",
49640
+ "mcp__claudemesh__list_files",
49641
+ "mcp__claudemesh__file_status",
49642
+ "mcp__claudemesh__delete_file",
49643
+ "mcp__claudemesh__vector_store",
49644
+ "mcp__claudemesh__vector_search",
49645
+ "mcp__claudemesh__vector_delete",
49646
+ "mcp__claudemesh__list_collections",
49647
+ "mcp__claudemesh__graph_query",
49648
+ "mcp__claudemesh__graph_execute",
49649
+ "mcp__claudemesh__mesh_info",
49650
+ "mcp__claudemesh__ping_mesh",
49651
+ "mcp__claudemesh__message_status",
49652
+ "mcp__claudemesh__share_context",
49653
+ "mcp__claudemesh__get_context",
49654
+ "mcp__claudemesh__list_contexts",
49655
+ "mcp__claudemesh__create_task",
49656
+ "mcp__claudemesh__claim_task",
49657
+ "mcp__claudemesh__complete_task",
49658
+ "mcp__claudemesh__list_tasks",
49659
+ "mcp__claudemesh__create_stream",
49660
+ "mcp__claudemesh__publish",
49661
+ "mcp__claudemesh__subscribe",
49662
+ "mcp__claudemesh__list_streams",
49663
+ "mcp__claudemesh__mesh_execute",
49664
+ "mcp__claudemesh__mesh_query",
49665
+ "mcp__claudemesh__mesh_schema"
49666
+ ];
49667
+ function installAllowedTools() {
49668
+ const settings = readClaudeSettings();
49669
+ const existing = new Set(settings.allowedTools ?? []);
49670
+ const toAdd = CLAUDEMESH_TOOLS.filter((t) => !existing.has(t));
49671
+ if (toAdd.length > 0) {
49672
+ settings.allowedTools = [...Array.from(existing), ...toAdd];
49673
+ writeClaudeSettings(settings);
49674
+ }
49675
+ return { added: toAdd, unchanged: CLAUDEMESH_TOOLS.length - toAdd.length };
49676
+ }
49677
+ function uninstallAllowedTools() {
49678
+ if (!existsSync2(CLAUDE_SETTINGS))
49679
+ return 0;
49680
+ const settings = readClaudeSettings();
49681
+ const existing = settings.allowedTools ?? [];
49682
+ const toolSet = new Set(CLAUDEMESH_TOOLS);
49683
+ const kept = existing.filter((t) => !toolSet.has(t));
49684
+ const removed = existing.length - kept.length;
49685
+ if (removed > 0) {
49686
+ settings.allowedTools = kept;
49687
+ writeClaudeSettings(settings);
49688
+ }
49689
+ return removed;
49690
+ }
49402
49691
  function installHooks() {
49403
49692
  const settings = readClaudeSettings();
49404
49693
  const hooks = (settings.hooks ??= {}) ?? {};
@@ -49475,6 +49764,19 @@ function runInstall(args = []) {
49475
49764
  console.log(`✓ MCP server "${MCP_NAME}" ${action}`);
49476
49765
  console.log(dim(` config: ${CLAUDE_CONFIG}`));
49477
49766
  console.log(dim(` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`));
49767
+ try {
49768
+ const { added, unchanged } = installAllowedTools();
49769
+ if (added.length > 0) {
49770
+ console.log(`✓ allowedTools: ${added.length} claudemesh tools pre-approved${unchanged > 0 ? `, ${unchanged} already present` : ""}`);
49771
+ console.log(dim(` This lets claudemesh tools run without --dangerously-skip-permissions.`));
49772
+ console.log(dim(` Your existing allowedTools entries were preserved.`));
49773
+ } else {
49774
+ console.log(`✓ allowedTools: all ${unchanged} claudemesh tools already pre-approved`);
49775
+ }
49776
+ console.log(dim(` config: ${CLAUDE_SETTINGS}`));
49777
+ } catch (e) {
49778
+ console.error(`⚠ allowedTools update failed: ${e instanceof Error ? e.message : String(e)}`);
49779
+ }
49478
49780
  if (!skipHooks) {
49479
49781
  try {
49480
49782
  const { added, unchanged } = installHooks();
@@ -49508,6 +49810,16 @@ function runUninstall() {
49508
49810
  } else {
49509
49811
  console.log(`· MCP server "${MCP_NAME}" not present`);
49510
49812
  }
49813
+ try {
49814
+ const removed = uninstallAllowedTools();
49815
+ if (removed > 0) {
49816
+ console.log(`✓ allowedTools: ${removed} claudemesh tools removed`);
49817
+ } else {
49818
+ console.log("· No claudemesh allowedTools to remove");
49819
+ }
49820
+ } catch (e) {
49821
+ console.error(`⚠ allowedTools removal failed: ${e instanceof Error ? e.message : String(e)}`);
49822
+ }
49511
49823
  try {
49512
49824
  const removed = uninstallHooks();
49513
49825
  if (removed > 0) {
@@ -49523,6 +49835,7 @@ function runUninstall() {
49523
49835
  }
49524
49836
 
49525
49837
  // src/invite/parse.ts
49838
+ init_keypair();
49526
49839
  function validatePayload(obj) {
49527
49840
  if (!obj || typeof obj !== "object")
49528
49841
  throw new Error("invite payload is not an object");
@@ -49643,6 +49956,7 @@ async function enrollWithBroker2(args) {
49643
49956
  }
49644
49957
 
49645
49958
  // src/commands/join.ts
49959
+ init_keypair();
49646
49960
  init_config();
49647
49961
  init_env();
49648
49962
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
@@ -49911,12 +50225,12 @@ async function confirmPermissions() {
49911
50225
  const yellow = (s) => useColor ? `\x1B[33m${s}\x1B[39m` : s;
49912
50226
  console.log(yellow(bold2(" Autonomous mode")));
49913
50227
  console.log("");
49914
- console.log(" Claude will send and receive peer messages without asking");
49915
- console.log(" you first. Peers exchange text only no file access,");
49916
- console.log(" no tool calls, no code execution.");
50228
+ console.log(" Claude will run with --dangerously-skip-permissions, bypassing");
50229
+ console.log(" ALL permission prompts not just claudemesh tools.");
50230
+ console.log(" Peers exchange text only — no file access, no tool calls.");
49917
50231
  console.log("");
49918
- console.log(dim(" Same as: claude --dangerously-skip-permissions"));
49919
- console.log(dim(" Skip this prompt: claudemesh launch -y"));
50232
+ console.log(dim(" Without -y: only claudemesh tools are pre-approved (via allowedTools)."));
50233
+ console.log(dim(" Use -y for autonomous agents. Omit it for shared/multi-person meshes."));
49920
50234
  console.log("");
49921
50235
  const rl = createInterface({ input: process.stdin, output: process.stdout });
49922
50236
  return new Promise((resolve2, reject) => {
@@ -50089,7 +50403,7 @@ async function runLaunch(flags, rawArgs) {
50089
50403
  const claudeArgs = [
50090
50404
  "--dangerously-load-development-channels",
50091
50405
  "server:claudemesh",
50092
- "--dangerously-skip-permissions",
50406
+ ...args.skipPermConfirm ? ["--dangerously-skip-permissions"] : [],
50093
50407
  ...args.systemPrompt ? ["--system-prompt", args.systemPrompt] : [],
50094
50408
  ...filtered
50095
50409
  ];
@@ -50142,7 +50456,7 @@ init_config();
50142
50456
  // package.json
50143
50457
  var package_default = {
50144
50458
  name: "claudemesh-cli",
50145
- version: "0.6.0",
50459
+ version: "0.6.2",
50146
50460
  description: "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
50147
50461
  keywords: [
50148
50462
  "claude-code",
@@ -50550,6 +50864,185 @@ function runWelcome() {
50550
50864
  console.log("");
50551
50865
  }
50552
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
+
50553
51046
  // src/index.ts
50554
51047
  var launch = defineCommand({
50555
51048
  meta: {
@@ -50672,6 +51165,69 @@ var main = defineCommand({
50672
51165
  }
50673
51166
  }),
50674
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
+ }),
50675
51231
  status: defineCommand({
50676
51232
  meta: { name: "status", description: "Check broker reachability for each joined mesh" },
50677
51233
  async run() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemesh-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
5
5
  "keywords": [
6
6
  "claude-code",