claudemesh-cli 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1125 -6
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -46941,6 +46941,46 @@ var TOOLS = [
46941
46941
  required: ["status"]
46942
46942
  }
46943
46943
  },
46944
+ {
46945
+ name: "set_visible",
46946
+ description: "Control your visibility in the mesh. When hidden, you won't appear in list_peers and won't receive broadcasts — but direct messages still reach you.",
46947
+ inputSchema: {
46948
+ type: "object",
46949
+ properties: {
46950
+ visible: {
46951
+ type: "boolean",
46952
+ description: "true to be visible (default), false to hide"
46953
+ }
46954
+ },
46955
+ required: ["visible"]
46956
+ }
46957
+ },
46958
+ {
46959
+ name: "set_profile",
46960
+ description: "Set your public profile — what other peers see about you. Avatar (emoji), title, bio, and capabilities list.",
46961
+ inputSchema: {
46962
+ type: "object",
46963
+ properties: {
46964
+ avatar: {
46965
+ type: "string",
46966
+ description: "Emoji or URL for your avatar"
46967
+ },
46968
+ title: {
46969
+ type: "string",
46970
+ description: "Short role label (e.g. 'Frontend Lead', 'DevOps')"
46971
+ },
46972
+ bio: {
46973
+ type: "string",
46974
+ description: "One-liner about yourself"
46975
+ },
46976
+ capabilities: {
46977
+ type: "array",
46978
+ items: { type: "string" },
46979
+ description: "What you can help with"
46980
+ }
46981
+ }
46982
+ }
46983
+ },
46944
46984
  {
46945
46985
  name: "join_group",
46946
46986
  description: "Join a group with an optional role. Other peers see your group membership in list_peers.",
@@ -47413,6 +47453,144 @@ var TOOLS = [
47413
47453
  description: "Get a complete overview of the mesh: peers, groups, state, memory, files, tasks, streams, tables. Call on session start for full situational awareness.",
47414
47454
  inputSchema: { type: "object", properties: {} }
47415
47455
  },
47456
+ {
47457
+ name: "mesh_stats",
47458
+ description: "View resource usage stats for all peers: messages sent/received, tool calls, uptime, errors.",
47459
+ inputSchema: { type: "object", properties: {} }
47460
+ },
47461
+ {
47462
+ name: "mesh_mcp_register",
47463
+ description: "Register an MCP server with the mesh. Other peers can invoke its tools through the mesh without restarting their sessions. Provide the server name, description, and full tool definitions.",
47464
+ inputSchema: {
47465
+ type: "object",
47466
+ properties: {
47467
+ server_name: { type: "string", description: "Unique name for the MCP server (e.g. 'github', 'jira')" },
47468
+ description: { type: "string", description: "What this MCP server does" },
47469
+ tools: {
47470
+ type: "array",
47471
+ items: {
47472
+ type: "object",
47473
+ properties: {
47474
+ name: { type: "string" },
47475
+ description: { type: "string" },
47476
+ inputSchema: { type: "object", description: "JSON Schema for tool arguments" }
47477
+ },
47478
+ required: ["name", "description", "inputSchema"]
47479
+ },
47480
+ description: "Tool definitions to expose"
47481
+ }
47482
+ },
47483
+ required: ["server_name", "description", "tools"]
47484
+ }
47485
+ },
47486
+ {
47487
+ name: "mesh_mcp_list",
47488
+ description: "List MCP servers available in the mesh with their tools. Shows which peer hosts each server.",
47489
+ inputSchema: { type: "object", properties: {} }
47490
+ },
47491
+ {
47492
+ name: "mesh_tool_call",
47493
+ description: "Call a tool on a mesh-registered MCP server. Route: you -> broker -> hosting peer -> execute -> result back. Timeout: 30s.",
47494
+ inputSchema: {
47495
+ type: "object",
47496
+ properties: {
47497
+ server_name: { type: "string", description: "Name of the MCP server" },
47498
+ tool_name: { type: "string", description: "Name of the tool to call" },
47499
+ args: { type: "object", description: "Tool arguments (JSON object)" }
47500
+ },
47501
+ required: ["server_name", "tool_name"]
47502
+ }
47503
+ },
47504
+ {
47505
+ name: "mesh_mcp_remove",
47506
+ description: "Unregister an MCP server you previously registered with the mesh.",
47507
+ inputSchema: {
47508
+ type: "object",
47509
+ properties: {
47510
+ server_name: { type: "string", description: "Name of the MCP server to remove" }
47511
+ },
47512
+ required: ["server_name"]
47513
+ }
47514
+ },
47515
+ {
47516
+ name: "mesh_set_clock",
47517
+ description: "Set the simulation clock speed. x1 = real-time, x10 = 10x faster, x100 = 100x. Peers receive heartbeat ticks at the simulated rate.",
47518
+ inputSchema: {
47519
+ type: "object",
47520
+ properties: {
47521
+ speed: {
47522
+ type: "number",
47523
+ description: "Speed multiplier (1-100). x1 = tick every 60s, x10 = tick every 6s, x100 = tick every 600ms."
47524
+ }
47525
+ },
47526
+ required: ["speed"]
47527
+ }
47528
+ },
47529
+ {
47530
+ name: "mesh_pause_clock",
47531
+ description: "Pause the simulation clock. Ticks stop until resumed.",
47532
+ inputSchema: { type: "object", properties: {} }
47533
+ },
47534
+ {
47535
+ name: "mesh_resume_clock",
47536
+ description: "Resume a paused simulation clock.",
47537
+ inputSchema: { type: "object", properties: {} }
47538
+ },
47539
+ {
47540
+ name: "mesh_clock",
47541
+ description: "Get current simulation clock status: speed, tick count, simulated time.",
47542
+ inputSchema: { type: "object", properties: {} }
47543
+ },
47544
+ {
47545
+ name: "share_skill",
47546
+ description: "Publish a reusable skill to the mesh. Other peers can discover and load it. If a skill with the same name exists, it is updated.",
47547
+ inputSchema: {
47548
+ type: "object",
47549
+ properties: {
47550
+ name: { type: "string", description: "Unique skill name (e.g. 'code-review', 'deploy-checklist')" },
47551
+ description: { type: "string", description: "Short description of what the skill does" },
47552
+ instructions: { type: "string", description: "Full instructions/prompt that a peer loads to acquire this capability" },
47553
+ tags: {
47554
+ type: "array",
47555
+ items: { type: "string" },
47556
+ description: "Tags for discoverability"
47557
+ }
47558
+ },
47559
+ required: ["name", "description", "instructions"]
47560
+ }
47561
+ },
47562
+ {
47563
+ name: "get_skill",
47564
+ description: "Load a skill's full instructions by name. Use to acquire capabilities shared by other peers.",
47565
+ inputSchema: {
47566
+ type: "object",
47567
+ properties: {
47568
+ name: { type: "string", description: "Skill name to load" }
47569
+ },
47570
+ required: ["name"]
47571
+ }
47572
+ },
47573
+ {
47574
+ name: "list_skills",
47575
+ description: "Browse available skills in the mesh. Optionally filter by keyword across name, description, and tags.",
47576
+ inputSchema: {
47577
+ type: "object",
47578
+ properties: {
47579
+ query: { type: "string", description: "Search keyword (optional)" }
47580
+ }
47581
+ }
47582
+ },
47583
+ {
47584
+ name: "remove_skill",
47585
+ description: "Remove a skill you published from the mesh.",
47586
+ inputSchema: {
47587
+ type: "object",
47588
+ properties: {
47589
+ name: { type: "string", description: "Skill name to remove" }
47590
+ },
47591
+ required: ["name"]
47592
+ }
47593
+ },
47416
47594
  {
47417
47595
  name: "ping_mesh",
47418
47596
  description: "Send test messages through the full pipeline and measure round-trip timing per priority. Diagnoses push delivery issues.",
@@ -47426,6 +47604,61 @@ var TOOLS = [
47426
47604
  }
47427
47605
  }
47428
47606
  }
47607
+ },
47608
+ {
47609
+ name: "read_peer_file",
47610
+ description: "Read a file from another peer's project. Specify the peer (by name) and the file path relative to their working directory. The peer must be online and sharing files. Max file size: 1MB.",
47611
+ inputSchema: {
47612
+ type: "object",
47613
+ properties: {
47614
+ peer: { type: "string", description: "Peer display name or pubkey" },
47615
+ path: { type: "string", description: "File path relative to peer's working directory" }
47616
+ },
47617
+ required: ["peer", "path"]
47618
+ }
47619
+ },
47620
+ {
47621
+ name: "list_peer_files",
47622
+ description: "List files in a peer's shared directory. Returns a tree of file names (not contents). The peer must be online and sharing files.",
47623
+ inputSchema: {
47624
+ type: "object",
47625
+ properties: {
47626
+ peer: { type: "string", description: "Peer display name or pubkey" },
47627
+ path: { type: "string", description: "Directory path relative to peer's cwd (default: root)" },
47628
+ pattern: { type: "string", description: "Glob-like filter pattern (e.g. '*.ts', 'src/*')" }
47629
+ },
47630
+ required: ["peer"]
47631
+ }
47632
+ },
47633
+ {
47634
+ name: "create_webhook",
47635
+ description: "Create an inbound webhook. Returns a URL that external services (GitHub, CI/CD, monitoring) can POST to — the payload becomes a mesh message to all peers.",
47636
+ inputSchema: {
47637
+ type: "object",
47638
+ properties: {
47639
+ name: {
47640
+ type: "string",
47641
+ description: "Webhook name (e.g. 'github-ci', 'datadog-alerts')"
47642
+ }
47643
+ },
47644
+ required: ["name"]
47645
+ }
47646
+ },
47647
+ {
47648
+ name: "list_webhooks",
47649
+ description: "List active webhooks for this mesh.",
47650
+ inputSchema: { type: "object", properties: {} }
47651
+ },
47652
+ {
47653
+ name: "delete_webhook",
47654
+ description: "Deactivate a webhook.",
47655
+ inputSchema: {
47656
+ type: "object",
47657
+ properties: {
47658
+ name: { type: "string", description: "Webhook name to deactivate" }
47659
+ },
47660
+ required: ["name"]
47661
+ }
47429
47662
  }
47430
47663
  ];
47431
47664
 
@@ -47511,10 +47744,21 @@ class BrokerClient {
47511
47744
  sessionPubkey = null;
47512
47745
  sessionSecretKey = null;
47513
47746
  grantFileAccessResolvers = new Map;
47747
+ peerFileResponseResolvers = new Map;
47748
+ peerDirResponseResolvers = new Map;
47749
+ sharedDirs = [process.cwd()];
47514
47750
  closed = false;
47515
47751
  reconnectAttempt = 0;
47516
47752
  helloTimer = null;
47517
47753
  reconnectTimer = null;
47754
+ _statsCounters = {
47755
+ messagesIn: 0,
47756
+ messagesOut: 0,
47757
+ toolCalls: 0,
47758
+ errors: 0
47759
+ };
47760
+ _sessionStartedAt = Date.now();
47761
+ _statsReportTimer = null;
47518
47762
  constructor(mesh, opts = {}) {
47519
47763
  this.mesh = mesh;
47520
47764
  this.opts = opts;
@@ -47596,6 +47840,7 @@ class BrokerClient {
47596
47840
  this.setConnStatus("open");
47597
47841
  this.reconnectAttempt = 0;
47598
47842
  this.flushOutbound();
47843
+ this.startStatsReporting();
47599
47844
  resolve();
47600
47845
  return;
47601
47846
  }
@@ -47635,6 +47880,7 @@ class BrokerClient {
47635
47880
  nonce = randomNonce();
47636
47881
  ciphertext = Buffer.from(message, "utf-8").toString("base64");
47637
47882
  }
47883
+ this._statsCounters.messagesOut++;
47638
47884
  return new Promise((resolve) => {
47639
47885
  if (this.pendingSends.size >= MAX_QUEUED) {
47640
47886
  resolve({ ok: false, error: "outbound queue full" });
@@ -47692,6 +47938,16 @@ class BrokerClient {
47692
47938
  return;
47693
47939
  this.ws.send(JSON.stringify({ type: "set_status", status }));
47694
47940
  }
47941
+ async setVisible(visible) {
47942
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47943
+ return;
47944
+ this.ws.send(JSON.stringify({ type: "set_visible", visible }));
47945
+ }
47946
+ async setProfile(profile) {
47947
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47948
+ return;
47949
+ this.ws.send(JSON.stringify({ type: "set_profile", ...profile }));
47950
+ }
47695
47951
  async listPeers() {
47696
47952
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47697
47953
  return [];
@@ -47709,6 +47965,34 @@ class BrokerClient {
47709
47965
  return;
47710
47966
  this.ws.send(JSON.stringify({ type: "set_summary", summary }));
47711
47967
  }
47968
+ setStats(stats) {
47969
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47970
+ return;
47971
+ const payload = stats ?? {
47972
+ ...this._statsCounters,
47973
+ uptime: Math.round((Date.now() - this._sessionStartedAt) / 1000)
47974
+ };
47975
+ this.ws.send(JSON.stringify({ type: "set_stats", stats: payload }));
47976
+ }
47977
+ incrementToolCalls() {
47978
+ this._statsCounters.toolCalls++;
47979
+ }
47980
+ incrementErrors() {
47981
+ this._statsCounters.errors++;
47982
+ }
47983
+ startStatsReporting() {
47984
+ if (this._statsReportTimer)
47985
+ return;
47986
+ this._statsReportTimer = setInterval(() => {
47987
+ this.setStats();
47988
+ }, 60000);
47989
+ }
47990
+ stopStatsReporting() {
47991
+ if (this._statsReportTimer) {
47992
+ clearInterval(this._statsReportTimer);
47993
+ this._statsReportTimer = null;
47994
+ }
47995
+ }
47712
47996
  async joinGroup(name, role) {
47713
47997
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47714
47998
  return;
@@ -47841,6 +48125,10 @@ class BrokerClient {
47841
48125
  scheduledAckResolvers = new Map;
47842
48126
  scheduledListResolvers = new Map;
47843
48127
  cancelScheduledResolvers = new Map;
48128
+ mcpRegisterResolvers = new Map;
48129
+ mcpListResolvers = new Map;
48130
+ mcpCallResolvers = new Map;
48131
+ mcpCallForwardHandler = null;
47844
48132
  async messageStatus(messageId) {
47845
48133
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47846
48134
  return null;
@@ -48139,7 +48427,105 @@ class BrokerClient {
48139
48427
  this.stateChangeHandlers.add(handler);
48140
48428
  return () => this.stateChangeHandlers.delete(handler);
48141
48429
  }
48430
+ async mcpRegister(serverName, description, tools) {
48431
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48432
+ return null;
48433
+ return new Promise((resolve) => {
48434
+ const reqId = this.makeReqId();
48435
+ this.mcpRegisterResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48436
+ if (this.mcpRegisterResolvers.delete(reqId))
48437
+ resolve(null);
48438
+ }, 5000) });
48439
+ this.ws.send(JSON.stringify({ type: "mcp_register", serverName, description, tools, _reqId: reqId }));
48440
+ });
48441
+ }
48442
+ async mcpUnregister(serverName) {
48443
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48444
+ return;
48445
+ this.ws.send(JSON.stringify({ type: "mcp_unregister", serverName }));
48446
+ }
48447
+ async mcpList() {
48448
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48449
+ return [];
48450
+ return new Promise((resolve) => {
48451
+ const reqId = this.makeReqId();
48452
+ this.mcpListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48453
+ if (this.mcpListResolvers.delete(reqId))
48454
+ resolve([]);
48455
+ }, 5000) });
48456
+ this.ws.send(JSON.stringify({ type: "mcp_list", _reqId: reqId }));
48457
+ });
48458
+ }
48459
+ async mcpCall(serverName, toolName, args) {
48460
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48461
+ return { error: "not connected" };
48462
+ return new Promise((resolve) => {
48463
+ const reqId = this.makeReqId();
48464
+ this.mcpCallResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48465
+ if (this.mcpCallResolvers.delete(reqId))
48466
+ resolve({ error: "MCP call timed out (30s)" });
48467
+ }, 30000) });
48468
+ this.ws.send(JSON.stringify({ type: "mcp_call", serverName, toolName, args, _reqId: reqId }));
48469
+ });
48470
+ }
48471
+ onMcpCallForward(handler) {
48472
+ this.mcpCallForwardHandler = handler;
48473
+ }
48474
+ sendMcpCallResponse(callId, result, error2) {
48475
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48476
+ return;
48477
+ this.ws.send(JSON.stringify({ type: "mcp_call_response", callId, result, error: error2 }));
48478
+ }
48142
48479
  meshInfoResolvers = new Map;
48480
+ clockStatusResolvers = new Map;
48481
+ async setClock(speed) {
48482
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48483
+ return null;
48484
+ return new Promise((resolve) => {
48485
+ const reqId = this.makeReqId();
48486
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48487
+ if (this.clockStatusResolvers.delete(reqId))
48488
+ resolve(null);
48489
+ }, 5000) });
48490
+ this.ws.send(JSON.stringify({ type: "set_clock", speed, _reqId: reqId }));
48491
+ });
48492
+ }
48493
+ async pauseClock() {
48494
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48495
+ return null;
48496
+ return new Promise((resolve) => {
48497
+ const reqId = this.makeReqId();
48498
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48499
+ if (this.clockStatusResolvers.delete(reqId))
48500
+ resolve(null);
48501
+ }, 5000) });
48502
+ this.ws.send(JSON.stringify({ type: "pause_clock", _reqId: reqId }));
48503
+ });
48504
+ }
48505
+ async resumeClock() {
48506
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48507
+ return null;
48508
+ return new Promise((resolve) => {
48509
+ const reqId = this.makeReqId();
48510
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48511
+ if (this.clockStatusResolvers.delete(reqId))
48512
+ resolve(null);
48513
+ }, 5000) });
48514
+ this.ws.send(JSON.stringify({ type: "resume_clock", _reqId: reqId }));
48515
+ });
48516
+ }
48517
+ async getClock() {
48518
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48519
+ return null;
48520
+ return new Promise((resolve) => {
48521
+ const reqId = this.makeReqId();
48522
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48523
+ if (this.clockStatusResolvers.delete(reqId))
48524
+ resolve(null);
48525
+ }, 5000) });
48526
+ this.ws.send(JSON.stringify({ type: "get_clock", _reqId: reqId }));
48527
+ });
48528
+ }
48143
48529
  async meshInfo() {
48144
48530
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48145
48531
  return null;
@@ -48152,8 +48538,132 @@ class BrokerClient {
48152
48538
  this.ws.send(JSON.stringify({ type: "mesh_info", _reqId: reqId }));
48153
48539
  });
48154
48540
  }
48541
+ skillAckResolvers = new Map;
48542
+ skillDataResolvers = new Map;
48543
+ skillListResolvers = new Map;
48544
+ async shareSkill(name, description, instructions, tags) {
48545
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48546
+ return null;
48547
+ return new Promise((resolve) => {
48548
+ const reqId = this.makeReqId();
48549
+ this.skillAckResolvers.set(reqId, { resolve: (result) => {
48550
+ resolve(result ? { ok: true, action: result.action } : null);
48551
+ }, timer: setTimeout(() => {
48552
+ if (this.skillAckResolvers.delete(reqId))
48553
+ resolve(null);
48554
+ }, 5000) });
48555
+ this.ws.send(JSON.stringify({ type: "share_skill", name, description, instructions, tags, _reqId: reqId }));
48556
+ });
48557
+ }
48558
+ async getSkill(name) {
48559
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48560
+ return null;
48561
+ return new Promise((resolve) => {
48562
+ const reqId = this.makeReqId();
48563
+ this.skillDataResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48564
+ if (this.skillDataResolvers.delete(reqId))
48565
+ resolve(null);
48566
+ }, 5000) });
48567
+ this.ws.send(JSON.stringify({ type: "get_skill", name, _reqId: reqId }));
48568
+ });
48569
+ }
48570
+ async listSkills(query) {
48571
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48572
+ return [];
48573
+ return new Promise((resolve) => {
48574
+ const reqId = this.makeReqId();
48575
+ this.skillListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48576
+ if (this.skillListResolvers.delete(reqId))
48577
+ resolve([]);
48578
+ }, 5000) });
48579
+ this.ws.send(JSON.stringify({ type: "list_skills", query, _reqId: reqId }));
48580
+ });
48581
+ }
48582
+ async removeSkill(name) {
48583
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48584
+ return false;
48585
+ return new Promise((resolve) => {
48586
+ const reqId = this.makeReqId();
48587
+ this.skillAckResolvers.set(reqId, { resolve: (result) => {
48588
+ resolve(result?.action === "removed");
48589
+ }, timer: setTimeout(() => {
48590
+ if (this.skillAckResolvers.delete(reqId))
48591
+ resolve(false);
48592
+ }, 5000) });
48593
+ this.ws.send(JSON.stringify({ type: "remove_skill", name, _reqId: reqId }));
48594
+ });
48595
+ }
48596
+ webhookAckResolvers = new Map;
48597
+ webhookListResolvers = new Map;
48598
+ async createWebhook(name) {
48599
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48600
+ return null;
48601
+ return new Promise((resolve) => {
48602
+ const reqId = this.makeReqId();
48603
+ this.webhookAckResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48604
+ if (this.webhookAckResolvers.delete(reqId))
48605
+ resolve(null);
48606
+ }, 5000) });
48607
+ this.ws.send(JSON.stringify({ type: "create_webhook", name, _reqId: reqId }));
48608
+ });
48609
+ }
48610
+ async listWebhooks() {
48611
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48612
+ return [];
48613
+ return new Promise((resolve) => {
48614
+ const reqId = this.makeReqId();
48615
+ this.webhookListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48616
+ if (this.webhookListResolvers.delete(reqId))
48617
+ resolve([]);
48618
+ }, 5000) });
48619
+ this.ws.send(JSON.stringify({ type: "list_webhooks", _reqId: reqId }));
48620
+ });
48621
+ }
48622
+ async deleteWebhook(name) {
48623
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48624
+ return false;
48625
+ return new Promise((resolve) => {
48626
+ const reqId = this.makeReqId();
48627
+ this.webhookAckResolvers.set(reqId, { resolve: () => resolve(true), timer: setTimeout(() => {
48628
+ if (this.webhookAckResolvers.delete(reqId))
48629
+ resolve(false);
48630
+ }, 5000) });
48631
+ this.ws.send(JSON.stringify({ type: "delete_webhook", name, _reqId: reqId }));
48632
+ });
48633
+ }
48634
+ setSharedDirs(dirs) {
48635
+ this.sharedDirs = dirs.map((d) => {
48636
+ const { resolve } = __require("node:path");
48637
+ return resolve(d);
48638
+ });
48639
+ }
48640
+ async requestFile(targetPubkey, filePath) {
48641
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48642
+ return { error: "not connected" };
48643
+ return new Promise((resolve) => {
48644
+ const reqId = this.makeReqId();
48645
+ this.peerFileResponseResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48646
+ if (this.peerFileResponseResolvers.delete(reqId))
48647
+ resolve({ error: "timeout waiting for peer response" });
48648
+ }, 15000) });
48649
+ this.ws.send(JSON.stringify({ type: "peer_file_request", targetPubkey, filePath, _reqId: reqId }));
48650
+ });
48651
+ }
48652
+ async requestDir(targetPubkey, dirPath, pattern) {
48653
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48654
+ return { error: "not connected" };
48655
+ return new Promise((resolve) => {
48656
+ const reqId = this.makeReqId();
48657
+ this.peerDirResponseResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48658
+ if (this.peerDirResponseResolvers.delete(reqId))
48659
+ resolve({ error: "timeout waiting for peer response" });
48660
+ }, 15000) });
48661
+ this.ws.send(JSON.stringify({ type: "peer_dir_request", targetPubkey, dirPath, ...pattern ? { pattern } : {}, _reqId: reqId }));
48662
+ });
48663
+ }
48155
48664
  close() {
48156
48665
  this.closed = true;
48666
+ this.stopStatsReporting();
48157
48667
  if (this.helloTimer)
48158
48668
  clearTimeout(this.helloTimer);
48159
48669
  if (this.reconnectTimer)
@@ -48165,6 +48675,141 @@ class BrokerClient {
48165
48675
  }
48166
48676
  this.setConnStatus("closed");
48167
48677
  }
48678
+ static MAX_FILE_SIZE = 1048576;
48679
+ async handlePeerFileRequest(msg) {
48680
+ const { resolve, join: join2, normalize } = await import("node:path");
48681
+ const { readFileSync: readFileSync2, statSync } = await import("node:fs");
48682
+ const reqId = msg._reqId;
48683
+ const sendResponse = (content, error2) => {
48684
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48685
+ return;
48686
+ this.ws.send(JSON.stringify({
48687
+ type: "peer_file_response",
48688
+ requesterPubkey: msg.requesterPubkey,
48689
+ filePath: msg.filePath,
48690
+ ...content !== undefined ? { content } : {},
48691
+ ...error2 ? { error: error2 } : {},
48692
+ ...reqId ? { _reqId: reqId } : {}
48693
+ }));
48694
+ };
48695
+ if (msg.filePath.includes("..")) {
48696
+ sendResponse(undefined, "path traversal not allowed");
48697
+ return;
48698
+ }
48699
+ let resolvedPath = null;
48700
+ for (const dir of this.sharedDirs) {
48701
+ const candidate = resolve(join2(dir, msg.filePath));
48702
+ const normalizedCandidate = normalize(candidate);
48703
+ const normalizedDir = normalize(dir);
48704
+ if (normalizedCandidate.startsWith(normalizedDir + "/") || normalizedCandidate === normalizedDir) {
48705
+ resolvedPath = candidate;
48706
+ break;
48707
+ }
48708
+ }
48709
+ if (!resolvedPath) {
48710
+ sendResponse(undefined, "file outside shared directories");
48711
+ return;
48712
+ }
48713
+ try {
48714
+ const stat = statSync(resolvedPath);
48715
+ if (!stat.isFile()) {
48716
+ sendResponse(undefined, "not a file");
48717
+ return;
48718
+ }
48719
+ if (stat.size > BrokerClient.MAX_FILE_SIZE) {
48720
+ sendResponse(undefined, `file too large (${stat.size} bytes, max ${BrokerClient.MAX_FILE_SIZE})`);
48721
+ return;
48722
+ }
48723
+ const content = readFileSync2(resolvedPath);
48724
+ sendResponse(content.toString("base64"));
48725
+ } catch (e) {
48726
+ const errMsg = e instanceof Error ? e.message : String(e);
48727
+ if (errMsg.includes("ENOENT")) {
48728
+ sendResponse(undefined, "file not found");
48729
+ } else {
48730
+ sendResponse(undefined, `read error: ${errMsg}`);
48731
+ }
48732
+ }
48733
+ }
48734
+ async handlePeerDirRequest(msg) {
48735
+ const { resolve, join: join2, normalize, relative } = await import("node:path");
48736
+ const { readdirSync, statSync } = await import("node:fs");
48737
+ const reqId = msg._reqId;
48738
+ const sendResponse = (entries, error2) => {
48739
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48740
+ return;
48741
+ this.ws.send(JSON.stringify({
48742
+ type: "peer_dir_response",
48743
+ requesterPubkey: msg.requesterPubkey,
48744
+ dirPath: msg.dirPath,
48745
+ ...entries ? { entries } : {},
48746
+ ...error2 ? { error: error2 } : {},
48747
+ ...reqId ? { _reqId: reqId } : {}
48748
+ }));
48749
+ };
48750
+ const dirPath = msg.dirPath || ".";
48751
+ if (dirPath.includes("..")) {
48752
+ sendResponse(undefined, "path traversal not allowed");
48753
+ return;
48754
+ }
48755
+ let resolvedPath = null;
48756
+ for (const dir of this.sharedDirs) {
48757
+ const candidate = resolve(join2(dir, dirPath));
48758
+ const normalizedCandidate = normalize(candidate);
48759
+ const normalizedDir = normalize(dir);
48760
+ if (normalizedCandidate.startsWith(normalizedDir + "/") || normalizedCandidate === normalizedDir) {
48761
+ resolvedPath = candidate;
48762
+ break;
48763
+ }
48764
+ }
48765
+ if (!resolvedPath) {
48766
+ sendResponse(undefined, "directory outside shared directories");
48767
+ return;
48768
+ }
48769
+ try {
48770
+ const stat = statSync(resolvedPath);
48771
+ if (!stat.isDirectory()) {
48772
+ sendResponse(undefined, "not a directory");
48773
+ return;
48774
+ }
48775
+ const entries = [];
48776
+ const MAX_ENTRIES = 500;
48777
+ const MAX_DEPTH = 2;
48778
+ const pattern = msg.pattern ? new RegExp(msg.pattern.replace(/\*/g, ".*").replace(/\?/g, "."), "i") : null;
48779
+ const walk = (dir, depth) => {
48780
+ if (entries.length >= MAX_ENTRIES || depth > MAX_DEPTH)
48781
+ return;
48782
+ try {
48783
+ const items = readdirSync(dir, { withFileTypes: true });
48784
+ for (const item of items) {
48785
+ if (entries.length >= MAX_ENTRIES)
48786
+ break;
48787
+ if (item.name.startsWith("."))
48788
+ continue;
48789
+ const relPath = relative(resolvedPath, join2(dir, item.name));
48790
+ const label = item.isDirectory() ? relPath + "/" : relPath;
48791
+ if (pattern && !pattern.test(item.name)) {
48792
+ if (item.isDirectory())
48793
+ walk(join2(dir, item.name), depth + 1);
48794
+ continue;
48795
+ }
48796
+ entries.push(label);
48797
+ if (item.isDirectory())
48798
+ walk(join2(dir, item.name), depth + 1);
48799
+ }
48800
+ } catch {}
48801
+ };
48802
+ walk(resolvedPath, 0);
48803
+ sendResponse(entries.sort());
48804
+ } catch (e) {
48805
+ const errMsg = e instanceof Error ? e.message : String(e);
48806
+ if (errMsg.includes("ENOENT")) {
48807
+ sendResponse(undefined, "directory not found");
48808
+ } else {
48809
+ sendResponse(undefined, `read error: ${errMsg}`);
48810
+ }
48811
+ }
48812
+ }
48168
48813
  resolveFromMap(map2, reqId, value) {
48169
48814
  let entry = reqId ? map2.get(reqId) : undefined;
48170
48815
  if (!entry) {
@@ -48202,6 +48847,7 @@ class BrokerClient {
48202
48847
  return;
48203
48848
  }
48204
48849
  if (msg.type === "push") {
48850
+ this._statsCounters.messagesIn++;
48205
48851
  const nonce = String(msg.nonce ?? "");
48206
48852
  const ciphertext = String(msg.ciphertext ?? "");
48207
48853
  const senderPubkey = String(msg.senderPubkey ?? "");
@@ -48402,10 +49048,34 @@ class BrokerClient {
48402
49048
  }
48403
49049
  return;
48404
49050
  }
49051
+ if (msg.type === "clock_status") {
49052
+ this.resolveFromMap(this.clockStatusResolvers, msgReqId, {
49053
+ speed: Number(msg.speed ?? 0),
49054
+ paused: Boolean(msg.paused),
49055
+ tick: Number(msg.tick ?? 0),
49056
+ simTime: String(msg.simTime ?? ""),
49057
+ startedAt: String(msg.startedAt ?? "")
49058
+ });
49059
+ return;
49060
+ }
48405
49061
  if (msg.type === "mesh_info_result") {
48406
49062
  this.resolveFromMap(this.meshInfoResolvers, msgReqId, msg);
48407
49063
  return;
48408
49064
  }
49065
+ if (msg.type === "skill_ack") {
49066
+ this.resolveFromMap(this.skillAckResolvers, msgReqId, { name: String(msg.name ?? ""), action: String(msg.action ?? "") });
49067
+ return;
49068
+ }
49069
+ if (msg.type === "skill_data") {
49070
+ const skill = msg.skill;
49071
+ this.resolveFromMap(this.skillDataResolvers, msgReqId, skill ?? null);
49072
+ return;
49073
+ }
49074
+ if (msg.type === "skill_list") {
49075
+ const skills = msg.skills ?? [];
49076
+ this.resolveFromMap(this.skillListResolvers, msgReqId, skills);
49077
+ return;
49078
+ }
48409
49079
  if (msg.type === "scheduled_ack") {
48410
49080
  this.resolveFromMap(this.scheduledAckResolvers, msgReqId, {
48411
49081
  scheduledId: String(msg.scheduledId ?? ""),
@@ -48423,6 +49093,75 @@ class BrokerClient {
48423
49093
  this.resolveFromMap(this.cancelScheduledResolvers, msgReqId, Boolean(msg.ok));
48424
49094
  return;
48425
49095
  }
49096
+ if (msg.type === "mcp_register_ack") {
49097
+ this.resolveFromMap(this.mcpRegisterResolvers, msgReqId, {
49098
+ serverName: String(msg.serverName ?? ""),
49099
+ toolCount: Number(msg.toolCount ?? 0)
49100
+ });
49101
+ return;
49102
+ }
49103
+ if (msg.type === "mcp_list_result") {
49104
+ const servers = msg.servers ?? [];
49105
+ this.resolveFromMap(this.mcpListResolvers, msgReqId, servers);
49106
+ return;
49107
+ }
49108
+ if (msg.type === "mcp_call_result") {
49109
+ this.resolveFromMap(this.mcpCallResolvers, msgReqId, {
49110
+ ...msg.result !== undefined ? { result: msg.result } : {},
49111
+ ...msg.error ? { error: String(msg.error) } : {}
49112
+ });
49113
+ return;
49114
+ }
49115
+ if (msg.type === "mcp_call_forward") {
49116
+ const forward = {
49117
+ callId: String(msg.callId ?? ""),
49118
+ serverName: String(msg.serverName ?? ""),
49119
+ toolName: String(msg.toolName ?? ""),
49120
+ args: msg.args ?? {},
49121
+ callerName: String(msg.callerName ?? "")
49122
+ };
49123
+ if (this.mcpCallForwardHandler) {
49124
+ this.mcpCallForwardHandler(forward).then((res) => this.sendMcpCallResponse(forward.callId, res.result, res.error)).catch((e) => this.sendMcpCallResponse(forward.callId, undefined, e instanceof Error ? e.message : String(e)));
49125
+ } else {
49126
+ this.sendMcpCallResponse(forward.callId, undefined, "No MCP call handler registered on this peer");
49127
+ }
49128
+ return;
49129
+ }
49130
+ if (msg.type === "peer_file_request_forward") {
49131
+ this.handlePeerFileRequest(msg);
49132
+ return;
49133
+ }
49134
+ if (msg.type === "peer_file_response_forward") {
49135
+ this.resolveFromMap(this.peerFileResponseResolvers, msgReqId, {
49136
+ content: msg.content ? String(msg.content) : undefined,
49137
+ error: msg.error ? String(msg.error) : undefined
49138
+ });
49139
+ return;
49140
+ }
49141
+ if (msg.type === "peer_dir_request_forward") {
49142
+ this.handlePeerDirRequest(msg);
49143
+ return;
49144
+ }
49145
+ if (msg.type === "peer_dir_response_forward") {
49146
+ this.resolveFromMap(this.peerDirResponseResolvers, msgReqId, {
49147
+ entries: msg.entries ?? undefined,
49148
+ error: msg.error ? String(msg.error) : undefined
49149
+ });
49150
+ return;
49151
+ }
49152
+ if (msg.type === "webhook_ack") {
49153
+ this.resolveFromMap(this.webhookAckResolvers, msgReqId, {
49154
+ name: String(msg.name ?? ""),
49155
+ url: String(msg.url ?? ""),
49156
+ secret: String(msg.secret ?? "")
49157
+ });
49158
+ return;
49159
+ }
49160
+ if (msg.type === "webhook_list") {
49161
+ const webhooks = msg.webhooks ?? [];
49162
+ this.resolveFromMap(this.webhookListResolvers, msgReqId, webhooks);
49163
+ return;
49164
+ }
48426
49165
  if (msg.type === "error") {
48427
49166
  this.debug(`broker error: ${msg.code} ${msg.message}`);
48428
49167
  const id = msg.id ? String(msg.id) : null;
@@ -48465,7 +49204,18 @@ class BrokerClient {
48465
49204
  [this.taskCreatedResolvers, null],
48466
49205
  [this.streamCreatedResolvers, null],
48467
49206
  [this.listPeersResolvers, []],
48468
- [this.meshInfoResolvers, null]
49207
+ [this.meshInfoResolvers, null],
49208
+ [this.clockStatusResolvers, null],
49209
+ [this.mcpRegisterResolvers, null],
49210
+ [this.mcpListResolvers, []],
49211
+ [this.mcpCallResolvers, { error: "broker error" }],
49212
+ [this.skillAckResolvers, null],
49213
+ [this.skillDataResolvers, null],
49214
+ [this.skillListResolvers, []],
49215
+ [this.peerFileResponseResolvers, { error: "broker error" }],
49216
+ [this.peerDirResponseResolvers, { error: "broker error" }],
49217
+ [this.webhookAckResolvers, null],
49218
+ [this.webhookListResolvers, []]
48469
49219
  ];
48470
49220
  for (const [map2, defaultVal] of allMaps) {
48471
49221
  const first = map2.entries().next().value;
@@ -48668,6 +49418,8 @@ If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder
48668
49418
  | check_messages() | Drain buffered inbound messages (auto-pushed in most cases, use as fallback). |
48669
49419
  | set_summary(summary) | Set 1-2 sentence description of your current work, visible to all peers. |
48670
49420
  | set_status(status) | Override status: idle, working, or dnd. |
49421
+ | set_visible(visible) | Toggle visibility. Hidden peers skip list_peers and broadcasts; direct messages still arrive. |
49422
+ | set_profile(avatar?, title?, bio?, capabilities?) | Set public profile: emoji avatar, short title, bio, capabilities list. |
48671
49423
  | join_group(name, role?) | Join a @group with optional role (lead, member, observer, or any string). |
48672
49424
  | leave_group(name) | Leave a @group. |
48673
49425
  | set_state(key, value) | Write shared state; pushes change to all peers. |
@@ -48704,6 +49456,12 @@ If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder
48704
49456
  | schedule_reminder(message, in_seconds?, deliver_at?, to?) | Schedule a reminder to yourself (no \`to\`) or a delayed message to a peer/group. Delivered as a push with \`subtype: reminder\` in the channel meta. |
48705
49457
  | list_scheduled() | List pending scheduled reminders and messages. |
48706
49458
  | cancel_scheduled(id) | Cancel a pending scheduled item. |
49459
+ | read_peer_file(peer, path) | Read a file from another peer's project (max 1MB). |
49460
+ | list_peer_files(peer, path?, pattern?) | List files in a peer's shared directory. |
49461
+ | mesh_mcp_register(server_name, description, tools) | Register an MCP server with the mesh. Other peers can call its tools. |
49462
+ | mesh_mcp_list() | List MCP servers available in the mesh with their tools. |
49463
+ | mesh_tool_call(server_name, tool_name, args?) | Call a tool on a mesh-registered MCP server (30s timeout). |
49464
+ | mesh_mcp_remove(server_name) | Unregister an MCP server you registered. |
48707
49465
 
48708
49466
  If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
48709
49467
 
@@ -48767,6 +49525,9 @@ Your message mode is "${messageMode}".
48767
49525
  }));
48768
49526
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
48769
49527
  const { name, arguments: args } = req.params;
49528
+ for (const c of allClients()) {
49529
+ c.incrementToolCalls();
49530
+ }
48770
49531
  if (config2.meshes.length === 0) {
48771
49532
  return text("No meshes joined. Run `claudemesh join https://claudemesh.com/join/<token>` first.", true);
48772
49533
  }
@@ -48822,7 +49583,10 @@ No peers connected.`);
48822
49583
  meta2.push(`model:${p.model}`);
48823
49584
  const metaStr = meta2.length ? ` {${meta2.join(", ")}}` : "";
48824
49585
  const cwdStr = p.cwd ? ` cwd:${p.cwd}` : "";
48825
- return `- **${p.displayName}** [${p.status}]${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`;
49586
+ const profileAvatar = p.profile?.avatar ? `${p.profile.avatar} ` : "";
49587
+ const profileTitle = p.profile?.title ? ` (${p.profile.title})` : "";
49588
+ const hiddenTag = p.visible === false ? " [hidden]" : "";
49589
+ return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${hiddenTag}${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`;
48826
49590
  });
48827
49591
  sections.push(`${header}
48828
49592
  ${peerLines.join(`
@@ -48889,6 +49653,32 @@ ${drained.join(`
48889
49653
  await c.setStatus(s);
48890
49654
  return text(`Status set to ${s} across ${allClients().length} mesh(es).`);
48891
49655
  }
49656
+ case "set_visible": {
49657
+ const { visible } = args ?? {};
49658
+ if (visible === undefined)
49659
+ return text("set_visible: `visible` required", true);
49660
+ for (const c of allClients())
49661
+ await c.setVisible(visible);
49662
+ return text(visible ? "You are now visible to peers." : "You are now hidden. Direct messages still reach you, but you won't appear in list_peers or receive broadcasts.");
49663
+ }
49664
+ case "set_profile": {
49665
+ const { avatar, title, bio, capabilities } = args ?? {};
49666
+ const profile = { avatar, title, bio, capabilities };
49667
+ for (const c of allClients())
49668
+ await c.setProfile(profile);
49669
+ const parts = [];
49670
+ if (avatar)
49671
+ parts.push(`Avatar: ${avatar}`);
49672
+ if (title)
49673
+ parts.push(`Title: ${title}`);
49674
+ if (bio)
49675
+ parts.push(`Bio: ${bio}`);
49676
+ if (capabilities?.length)
49677
+ parts.push(`Capabilities: ${capabilities.join(", ")}`);
49678
+ return text(parts.length > 0 ? `Profile updated:
49679
+ ${parts.join(`
49680
+ `)}` : "Profile cleared.");
49681
+ }
48892
49682
  case "join_group": {
48893
49683
  const { name: groupName, role } = args ?? {};
48894
49684
  if (!groupName)
@@ -49422,6 +50212,72 @@ ${rows.join(`
49422
50212
  return text("No active streams.");
49423
50213
  const lines = streams.map((s) => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
49424
50214
  return text(lines.join(`
50215
+ `));
50216
+ }
50217
+ case "mesh_set_clock": {
50218
+ const { speed } = args ?? {};
50219
+ if (!speed || speed < 1 || speed > 100)
50220
+ return text("mesh_set_clock: speed must be 1-100", true);
50221
+ const client2 = allClients()[0];
50222
+ if (!client2)
50223
+ return text("mesh_set_clock: not connected", true);
50224
+ const result = await client2.setClock(speed);
50225
+ if (!result)
50226
+ return text("mesh_set_clock: timed out", true);
50227
+ return text([
50228
+ `**Clock set to x${result.speed}**`,
50229
+ `Paused: ${result.paused}`,
50230
+ `Tick: ${result.tick}`,
50231
+ `Sim time: ${result.simTime}`,
50232
+ `Started at: ${result.startedAt}`
50233
+ ].join(`
50234
+ `));
50235
+ }
50236
+ case "mesh_pause_clock": {
50237
+ const client2 = allClients()[0];
50238
+ if (!client2)
50239
+ return text("mesh_pause_clock: not connected", true);
50240
+ const result = await client2.pauseClock();
50241
+ if (!result)
50242
+ return text("mesh_pause_clock: timed out", true);
50243
+ return text([
50244
+ "**Clock paused**",
50245
+ `Speed: x${result.speed}`,
50246
+ `Tick: ${result.tick}`,
50247
+ `Sim time: ${result.simTime}`
50248
+ ].join(`
50249
+ `));
50250
+ }
50251
+ case "mesh_resume_clock": {
50252
+ const client2 = allClients()[0];
50253
+ if (!client2)
50254
+ return text("mesh_resume_clock: not connected", true);
50255
+ const result = await client2.resumeClock();
50256
+ if (!result)
50257
+ return text("mesh_resume_clock: timed out", true);
50258
+ return text([
50259
+ "**Clock resumed**",
50260
+ `Speed: x${result.speed}`,
50261
+ `Tick: ${result.tick}`,
50262
+ `Sim time: ${result.simTime}`
50263
+ ].join(`
50264
+ `));
50265
+ }
50266
+ case "mesh_clock": {
50267
+ const client2 = allClients()[0];
50268
+ if (!client2)
50269
+ return text("mesh_clock: not connected", true);
50270
+ const result = await client2.getClock();
50271
+ if (!result)
50272
+ return text("mesh_clock: timed out", true);
50273
+ const statusLabel = result.speed === 0 ? "not started" : result.paused ? "paused" : "running";
50274
+ return text([
50275
+ `**Clock status: ${statusLabel}**`,
50276
+ `Speed: x${result.speed}`,
50277
+ `Tick: ${result.tick}`,
50278
+ `Sim time: ${result.simTime}`,
50279
+ `Started at: ${result.startedAt}`
50280
+ ].join(`
49425
50281
  `));
49426
50282
  }
49427
50283
  case "mesh_info": {
@@ -49447,6 +50303,89 @@ ${rows.join(`
49447
50303
  return text(lines.join(`
49448
50304
  `));
49449
50305
  }
50306
+ case "mesh_stats": {
50307
+ const clients2 = allClients();
50308
+ if (clients2.length === 0)
50309
+ return text("mesh_stats: no joined meshes", true);
50310
+ const sections = [];
50311
+ for (const c of clients2) {
50312
+ const peers = await c.listPeers();
50313
+ const header = `## ${c.meshSlug}`;
50314
+ const rows = peers.map((p) => {
50315
+ const s = p.stats;
50316
+ if (!s)
50317
+ return `| ${p.displayName} | - | - | - | - | - |`;
50318
+ const up = s.uptime != null ? `${Math.floor(s.uptime / 60)}m` : "-";
50319
+ return `| ${p.displayName} | ${s.messagesIn ?? 0} | ${s.messagesOut ?? 0} | ${s.toolCalls ?? 0} | ${up} | ${s.errors ?? 0} |`;
50320
+ });
50321
+ sections.push(`${header}
50322
+ | Peer | Msgs In | Msgs Out | Tool Calls | Uptime | Errors |
50323
+ |------|---------|----------|------------|--------|--------|
50324
+ ${rows.join(`
50325
+ `)}`);
50326
+ }
50327
+ return text(sections.join(`
50328
+
50329
+ `));
50330
+ }
50331
+ case "share_skill": {
50332
+ const { name: skillName, description: skillDesc, instructions: skillInstr, tags: skillTags } = args ?? {};
50333
+ if (!skillName || !skillDesc || !skillInstr)
50334
+ return text("share_skill: `name`, `description`, and `instructions` required", true);
50335
+ const client2 = allClients()[0];
50336
+ if (!client2)
50337
+ return text("share_skill: not connected", true);
50338
+ const result = await client2.shareSkill(skillName, skillDesc, skillInstr, skillTags);
50339
+ if (!result)
50340
+ return text("share_skill: broker did not acknowledge", true);
50341
+ return text(`Skill "${skillName}" published to the mesh.`);
50342
+ }
50343
+ case "get_skill": {
50344
+ const { name: gsName } = args ?? {};
50345
+ if (!gsName)
50346
+ return text("get_skill: `name` required", true);
50347
+ const client2 = allClients()[0];
50348
+ if (!client2)
50349
+ return text("get_skill: not connected", true);
50350
+ const skill = await client2.getSkill(gsName);
50351
+ if (!skill)
50352
+ return text(`Skill "${gsName}" not found in the mesh.`);
50353
+ return text(`# Skill: ${skill.name}
50354
+
50355
+ **Description:** ${skill.description}
50356
+ **Author:** ${skill.author}
50357
+ **Tags:** ${skill.tags.length ? skill.tags.join(", ") : "none"}
50358
+ **Created:** ${skill.createdAt}
50359
+
50360
+ ---
50361
+
50362
+ ## Instructions
50363
+
50364
+ ${skill.instructions}`);
50365
+ }
50366
+ case "list_skills": {
50367
+ const { query: skillQuery } = args ?? {};
50368
+ const client2 = allClients()[0];
50369
+ if (!client2)
50370
+ return text("list_skills: not connected", true);
50371
+ const skills = await client2.listSkills(skillQuery);
50372
+ if (skills.length === 0)
50373
+ return text(skillQuery ? `No skills found for "${skillQuery}".` : "No skills in the mesh yet.");
50374
+ const lines = skills.map((s) => `- **${s.name}**: ${s.description}${s.tags.length ? ` [${s.tags.join(", ")}]` : ""} (by ${s.author})`);
50375
+ return text(`${skills.length} skill(s):
50376
+ ${lines.join(`
50377
+ `)}`);
50378
+ }
50379
+ case "remove_skill": {
50380
+ const { name: rsName } = args ?? {};
50381
+ if (!rsName)
50382
+ return text("remove_skill: `name` required", true);
50383
+ const client2 = allClients()[0];
50384
+ if (!client2)
50385
+ return text("remove_skill: not connected", true);
50386
+ const removed = await client2.removeSkill(rsName);
50387
+ return text(removed ? `Skill "${rsName}" removed.` : `Skill "${rsName}" not found.`, !removed);
50388
+ }
49450
50389
  case "ping_mesh": {
49451
50390
  const { priorities: pingPriorities } = args ?? {};
49452
50391
  const toTest = pingPriorities ?? ["now", "next"];
@@ -49483,6 +50422,57 @@ ${rows.join(`
49483
50422
  return text(results.join(`
49484
50423
  `));
49485
50424
  }
50425
+ case "mesh_mcp_register": {
50426
+ const { server_name, description, tools: regTools } = args ?? {};
50427
+ if (!server_name || !description || !regTools?.length)
50428
+ return text("mesh_mcp_register: `server_name`, `description`, and `tools` required", true);
50429
+ const client2 = allClients()[0];
50430
+ if (!client2)
50431
+ return text("mesh_mcp_register: not connected", true);
50432
+ const result = await client2.mcpRegister(server_name, description, regTools);
50433
+ if (!result)
50434
+ return text("mesh_mcp_register: broker did not acknowledge", true);
50435
+ return text(`Registered MCP server "${result.serverName}" with ${result.toolCount} tool(s). Other peers can now call its tools via mesh_tool_call.`);
50436
+ }
50437
+ case "mesh_mcp_list": {
50438
+ const client2 = allClients()[0];
50439
+ if (!client2)
50440
+ return text("mesh_mcp_list: not connected", true);
50441
+ const servers = await client2.mcpList();
50442
+ if (servers.length === 0)
50443
+ return text("No MCP servers registered in the mesh.");
50444
+ const lines = servers.map((s) => {
50445
+ const toolList = s.tools.map((t) => ` - **${t.name}**: ${t.description}`).join(`
50446
+ `);
50447
+ return `- **${s.name}** (hosted by ${s.hostedBy}): ${s.description}
50448
+ ${toolList}`;
50449
+ });
50450
+ return text(`${servers.length} MCP server(s) in mesh:
50451
+ ${lines.join(`
50452
+ `)}`);
50453
+ }
50454
+ case "mesh_tool_call": {
50455
+ const { server_name: callServer, tool_name: callTool, args: callArgs } = args ?? {};
50456
+ if (!callServer || !callTool)
50457
+ return text("mesh_tool_call: `server_name` and `tool_name` required", true);
50458
+ const client2 = allClients()[0];
50459
+ if (!client2)
50460
+ return text("mesh_tool_call: not connected", true);
50461
+ const callResult = await client2.mcpCall(callServer, callTool, callArgs ?? {});
50462
+ if (callResult.error)
50463
+ return text(`mesh_tool_call error: ${callResult.error}`, true);
50464
+ return text(typeof callResult.result === "string" ? callResult.result : JSON.stringify(callResult.result, null, 2));
50465
+ }
50466
+ case "mesh_mcp_remove": {
50467
+ const { server_name: rmServer } = args ?? {};
50468
+ if (!rmServer)
50469
+ return text("mesh_mcp_remove: `server_name` required", true);
50470
+ const client2 = allClients()[0];
50471
+ if (!client2)
50472
+ return text("mesh_mcp_remove: not connected", true);
50473
+ await client2.mcpUnregister(rmServer);
50474
+ return text(`Unregistered MCP server "${rmServer}" from the mesh.`);
50475
+ }
49486
50476
  case "grant_file_access": {
49487
50477
  const { fileId, to: grantTo } = args ?? {};
49488
50478
  if (!fileId || !grantTo)
@@ -49515,6 +50505,117 @@ ${rows.join(`
49515
50505
  return text("grant_file_access: broker did not confirm", true);
49516
50506
  return text(`Access granted: ${targetPeer.displayName} can now download file ${fileId}`);
49517
50507
  }
50508
+ case "read_peer_file": {
50509
+ const { peer: peerName, path: filePath } = args ?? {};
50510
+ if (!peerName || !filePath)
50511
+ return text("read_peer_file: `peer` and `path` required", true);
50512
+ const client2 = allClients()[0];
50513
+ if (!client2)
50514
+ return text("read_peer_file: not connected", true);
50515
+ const peers = await client2.listPeers();
50516
+ const nameLower = peerName.toLowerCase();
50517
+ let targetPubkey = null;
50518
+ if (/^[0-9a-f]{64}$/.test(peerName)) {
50519
+ targetPubkey = peerName;
50520
+ } else {
50521
+ const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
50522
+ if (!match) {
50523
+ const partials = peers.filter((p) => p.displayName.toLowerCase().includes(nameLower));
50524
+ if (partials.length === 1) {
50525
+ targetPubkey = partials[0].pubkey;
50526
+ } else {
50527
+ const names = peers.map((p) => p.displayName).join(", ");
50528
+ return text(`read_peer_file: peer "${peerName}" not found. Online: ${names || "(none)"}`, true);
50529
+ }
50530
+ } else {
50531
+ targetPubkey = match.pubkey;
50532
+ }
50533
+ }
50534
+ const result = await client2.requestFile(targetPubkey, filePath);
50535
+ if (result.error)
50536
+ return text(`read_peer_file: ${result.error}`, true);
50537
+ if (!result.content)
50538
+ return text("read_peer_file: empty response from peer", true);
50539
+ try {
50540
+ const decoded = Buffer.from(result.content, "base64").toString("utf-8");
50541
+ return text(decoded);
50542
+ } catch {
50543
+ return text("read_peer_file: failed to decode file content (binary file?)", true);
50544
+ }
50545
+ }
50546
+ case "list_peer_files": {
50547
+ const { peer: peerName, path: dirPath, pattern } = args ?? {};
50548
+ if (!peerName)
50549
+ return text("list_peer_files: `peer` required", true);
50550
+ const client2 = allClients()[0];
50551
+ if (!client2)
50552
+ return text("list_peer_files: not connected", true);
50553
+ const peers = await client2.listPeers();
50554
+ const nameLower = peerName.toLowerCase();
50555
+ let targetPubkey = null;
50556
+ if (/^[0-9a-f]{64}$/.test(peerName)) {
50557
+ targetPubkey = peerName;
50558
+ } else {
50559
+ const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
50560
+ if (!match) {
50561
+ const partials = peers.filter((p) => p.displayName.toLowerCase().includes(nameLower));
50562
+ if (partials.length === 1) {
50563
+ targetPubkey = partials[0].pubkey;
50564
+ } else {
50565
+ const names = peers.map((p) => p.displayName).join(", ");
50566
+ return text(`list_peer_files: peer "${peerName}" not found. Online: ${names || "(none)"}`, true);
50567
+ }
50568
+ } else {
50569
+ targetPubkey = match.pubkey;
50570
+ }
50571
+ }
50572
+ const result = await client2.requestDir(targetPubkey, dirPath ?? ".", pattern);
50573
+ if (result.error)
50574
+ return text(`list_peer_files: ${result.error}`, true);
50575
+ if (!result.entries || result.entries.length === 0)
50576
+ return text("No files found.");
50577
+ return text(result.entries.join(`
50578
+ `));
50579
+ }
50580
+ case "create_webhook": {
50581
+ const { name: whName } = args ?? {};
50582
+ if (!whName)
50583
+ return text("create_webhook: `name` required", true);
50584
+ const client2 = allClients()[0];
50585
+ if (!client2)
50586
+ return text("create_webhook: not connected", true);
50587
+ const wh = await client2.createWebhook(whName);
50588
+ if (!wh)
50589
+ return text("create_webhook: broker did not acknowledge — check connection", true);
50590
+ return text(`Webhook **${wh.name}** created.
50591
+
50592
+ URL: ${wh.url}
50593
+ Secret: ${wh.secret}
50594
+
50595
+ External services can POST JSON to this URL. The payload will be pushed to all connected mesh peers.`);
50596
+ }
50597
+ case "list_webhooks": {
50598
+ const client2 = allClients()[0];
50599
+ if (!client2)
50600
+ return text("list_webhooks: not connected", true);
50601
+ const webhooks = await client2.listWebhooks();
50602
+ if (webhooks.length === 0)
50603
+ return text("No active webhooks.");
50604
+ const lines = webhooks.map((w) => `- **${w.name}** — ${w.url} (created ${w.createdAt})`);
50605
+ return text(`${webhooks.length} webhook(s):
50606
+ ${lines.join(`
50607
+ `)}`);
50608
+ }
50609
+ case "delete_webhook": {
50610
+ const { name: delName } = args ?? {};
50611
+ if (!delName)
50612
+ return text("delete_webhook: `name` required", true);
50613
+ const client2 = allClients()[0];
50614
+ if (!client2)
50615
+ return text("delete_webhook: not connected", true);
50616
+ const ok = await client2.deleteWebhook(delName);
50617
+ return text(ok ? `Webhook "${delName}" deactivated.` : `Failed to deactivate webhook "${delName}".`, !ok);
50618
+ }
49518
50619
  default:
49519
50620
  return text(`Unknown tool: ${name}`, true);
49520
50621
  }
@@ -49530,7 +50631,12 @@ ${rows.join(`
49530
50631
  const eventName = msg.event;
49531
50632
  const data = msg.eventData ?? {};
49532
50633
  let content2;
49533
- if (eventName === "peer_joined") {
50634
+ if (eventName === "tick") {
50635
+ const tick = data.tick ?? 0;
50636
+ const simTime = String(data.simTime ?? "").replace("T", " ").replace(/\..*/, "");
50637
+ const speed = data.speed ?? 1;
50638
+ content2 = `[heartbeat] tick ${tick} | sim time: ${simTime} | speed: x${speed}`;
50639
+ } else if (eventName === "peer_joined") {
49534
50640
  content2 = `[system] Peer "${data.name ?? "unknown"}" joined the mesh`;
49535
50641
  } else if (eventName === "peer_left") {
49536
50642
  content2 = `[system] Peer "${data.name ?? "unknown"}" left the mesh`;
@@ -49657,6 +50763,7 @@ ${rows.join(`
49657
50763
  }
49658
50764
 
49659
50765
  // src/commands/install.ts
50766
+ init_config();
49660
50767
  import {
49661
50768
  chmodSync as chmodSync2,
49662
50769
  copyFileSync,
@@ -49957,10 +51064,22 @@ function runInstall(args = []) {
49957
51064
  } else {
49958
51065
  console.log(dim("· Hooks skipped (--no-hooks)"));
49959
51066
  }
51067
+ let hasMeshes = false;
51068
+ try {
51069
+ const meshConfig = loadConfig();
51070
+ hasMeshes = meshConfig.meshes.length > 0;
51071
+ } catch {}
49960
51072
  console.log("");
49961
51073
  console.log(yellow(bold2("⚠ RESTART CLAUDE CODE")) + yellow(" for MCP tools to appear."));
49962
- console.log("");
49963
- console.log(`Next: ${bold2("claudemesh join https://claudemesh.com/join/<token>")}`);
51074
+ if (!hasMeshes) {
51075
+ console.log("");
51076
+ console.log(yellow("No meshes joined.") + " To connect with peers:");
51077
+ console.log(` ${bold2("claudemesh join <invite-url>")}` + dim(" — join an existing mesh"));
51078
+ console.log(` ${dim("Create one at")} ${bold2("https://claudemesh.com/dashboard")}`);
51079
+ } else {
51080
+ console.log("");
51081
+ console.log(`Next: ${bold2("claudemesh join https://claudemesh.com/join/<token>")}`);
51082
+ }
49964
51083
  console.log("");
49965
51084
  console.log(yellow("⚠ For real-time push messages from peers, launch with:"));
49966
51085
  console.log(` ${bold2("claudemesh launch")}` + dim(" (or: claude --dangerously-load-development-channels server:claudemesh)"));
@@ -50620,7 +51739,7 @@ init_config();
50620
51739
  // package.json
50621
51740
  var package_default = {
50622
51741
  name: "claudemesh-cli",
50623
- version: "0.6.9",
51742
+ version: "0.7.0",
50624
51743
  description: "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
50625
51744
  keywords: [
50626
51745
  "claude-code",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemesh-cli",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
4
4
  "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -48,10 +48,10 @@
48
48
  "prettier": "3.6.2",
49
49
  "typescript": "5.9.3",
50
50
  "vitest": "4.0.14",
51
- "@turbostarter/eslint-config": "0.1.0",
52
- "@turbostarter/prettier-config": "0.1.0",
53
51
  "@turbostarter/tsconfig": "0.1.0",
54
- "@turbostarter/vitest-config": "0.1.0"
52
+ "@turbostarter/prettier-config": "0.1.0",
53
+ "@turbostarter/vitest-config": "0.1.0",
54
+ "@turbostarter/eslint-config": "0.1.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "bun build src/index.ts --target=node --outfile dist/index.js --banner \"#!/usr/bin/env node\" && chmod +x dist/index.js",