claudemesh-cli 0.6.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1197 -9
  2. package/package.json +2 -2
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,148 @@ 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
+ persistent: {
47483
+ type: "boolean",
47484
+ description: "If true, registration survives peer disconnect. Other peers see it as 'offline' until you reconnect. Default: false"
47485
+ }
47486
+ },
47487
+ required: ["server_name", "description", "tools"]
47488
+ }
47489
+ },
47490
+ {
47491
+ name: "mesh_mcp_list",
47492
+ description: "List MCP servers available in the mesh with their tools. Shows which peer hosts each server.",
47493
+ inputSchema: { type: "object", properties: {} }
47494
+ },
47495
+ {
47496
+ name: "mesh_tool_call",
47497
+ description: "Call a tool on a mesh-registered MCP server. Route: you -> broker -> hosting peer -> execute -> result back. Timeout: 30s.",
47498
+ inputSchema: {
47499
+ type: "object",
47500
+ properties: {
47501
+ server_name: { type: "string", description: "Name of the MCP server" },
47502
+ tool_name: { type: "string", description: "Name of the tool to call" },
47503
+ args: { type: "object", description: "Tool arguments (JSON object)" }
47504
+ },
47505
+ required: ["server_name", "tool_name"]
47506
+ }
47507
+ },
47508
+ {
47509
+ name: "mesh_mcp_remove",
47510
+ description: "Unregister an MCP server you previously registered with the mesh.",
47511
+ inputSchema: {
47512
+ type: "object",
47513
+ properties: {
47514
+ server_name: { type: "string", description: "Name of the MCP server to remove" }
47515
+ },
47516
+ required: ["server_name"]
47517
+ }
47518
+ },
47519
+ {
47520
+ name: "mesh_set_clock",
47521
+ description: "Set the simulation clock speed. x1 = real-time, x10 = 10x faster, x100 = 100x. Peers receive heartbeat ticks at the simulated rate.",
47522
+ inputSchema: {
47523
+ type: "object",
47524
+ properties: {
47525
+ speed: {
47526
+ type: "number",
47527
+ description: "Speed multiplier (1-100). x1 = tick every 60s, x10 = tick every 6s, x100 = tick every 600ms."
47528
+ }
47529
+ },
47530
+ required: ["speed"]
47531
+ }
47532
+ },
47533
+ {
47534
+ name: "mesh_pause_clock",
47535
+ description: "Pause the simulation clock. Ticks stop until resumed.",
47536
+ inputSchema: { type: "object", properties: {} }
47537
+ },
47538
+ {
47539
+ name: "mesh_resume_clock",
47540
+ description: "Resume a paused simulation clock.",
47541
+ inputSchema: { type: "object", properties: {} }
47542
+ },
47543
+ {
47544
+ name: "mesh_clock",
47545
+ description: "Get current simulation clock status: speed, tick count, simulated time.",
47546
+ inputSchema: { type: "object", properties: {} }
47547
+ },
47548
+ {
47549
+ name: "share_skill",
47550
+ 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.",
47551
+ inputSchema: {
47552
+ type: "object",
47553
+ properties: {
47554
+ name: { type: "string", description: "Unique skill name (e.g. 'code-review', 'deploy-checklist')" },
47555
+ description: { type: "string", description: "Short description of what the skill does" },
47556
+ instructions: { type: "string", description: "Full instructions/prompt that a peer loads to acquire this capability" },
47557
+ tags: {
47558
+ type: "array",
47559
+ items: { type: "string" },
47560
+ description: "Tags for discoverability"
47561
+ }
47562
+ },
47563
+ required: ["name", "description", "instructions"]
47564
+ }
47565
+ },
47566
+ {
47567
+ name: "get_skill",
47568
+ description: "Load a skill's full instructions by name. Use to acquire capabilities shared by other peers.",
47569
+ inputSchema: {
47570
+ type: "object",
47571
+ properties: {
47572
+ name: { type: "string", description: "Skill name to load" }
47573
+ },
47574
+ required: ["name"]
47575
+ }
47576
+ },
47577
+ {
47578
+ name: "list_skills",
47579
+ description: "Browse available skills in the mesh. Optionally filter by keyword across name, description, and tags.",
47580
+ inputSchema: {
47581
+ type: "object",
47582
+ properties: {
47583
+ query: { type: "string", description: "Search keyword (optional)" }
47584
+ }
47585
+ }
47586
+ },
47587
+ {
47588
+ name: "remove_skill",
47589
+ description: "Remove a skill you published from the mesh.",
47590
+ inputSchema: {
47591
+ type: "object",
47592
+ properties: {
47593
+ name: { type: "string", description: "Skill name to remove" }
47594
+ },
47595
+ required: ["name"]
47596
+ }
47597
+ },
47416
47598
  {
47417
47599
  name: "ping_mesh",
47418
47600
  description: "Send test messages through the full pipeline and measure round-trip timing per priority. Diagnoses push delivery issues.",
@@ -47426,6 +47608,61 @@ var TOOLS = [
47426
47608
  }
47427
47609
  }
47428
47610
  }
47611
+ },
47612
+ {
47613
+ name: "read_peer_file",
47614
+ 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.",
47615
+ inputSchema: {
47616
+ type: "object",
47617
+ properties: {
47618
+ peer: { type: "string", description: "Peer display name or pubkey" },
47619
+ path: { type: "string", description: "File path relative to peer's working directory" }
47620
+ },
47621
+ required: ["peer", "path"]
47622
+ }
47623
+ },
47624
+ {
47625
+ name: "list_peer_files",
47626
+ 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.",
47627
+ inputSchema: {
47628
+ type: "object",
47629
+ properties: {
47630
+ peer: { type: "string", description: "Peer display name or pubkey" },
47631
+ path: { type: "string", description: "Directory path relative to peer's cwd (default: root)" },
47632
+ pattern: { type: "string", description: "Glob-like filter pattern (e.g. '*.ts', 'src/*')" }
47633
+ },
47634
+ required: ["peer"]
47635
+ }
47636
+ },
47637
+ {
47638
+ name: "create_webhook",
47639
+ 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.",
47640
+ inputSchema: {
47641
+ type: "object",
47642
+ properties: {
47643
+ name: {
47644
+ type: "string",
47645
+ description: "Webhook name (e.g. 'github-ci', 'datadog-alerts')"
47646
+ }
47647
+ },
47648
+ required: ["name"]
47649
+ }
47650
+ },
47651
+ {
47652
+ name: "list_webhooks",
47653
+ description: "List active webhooks for this mesh.",
47654
+ inputSchema: { type: "object", properties: {} }
47655
+ },
47656
+ {
47657
+ name: "delete_webhook",
47658
+ description: "Deactivate a webhook.",
47659
+ inputSchema: {
47660
+ type: "object",
47661
+ properties: {
47662
+ name: { type: "string", description: "Webhook name to deactivate" }
47663
+ },
47664
+ required: ["name"]
47665
+ }
47429
47666
  }
47430
47667
  ];
47431
47668
 
@@ -47511,10 +47748,21 @@ class BrokerClient {
47511
47748
  sessionPubkey = null;
47512
47749
  sessionSecretKey = null;
47513
47750
  grantFileAccessResolvers = new Map;
47751
+ peerFileResponseResolvers = new Map;
47752
+ peerDirResponseResolvers = new Map;
47753
+ sharedDirs = [process.cwd()];
47514
47754
  closed = false;
47515
47755
  reconnectAttempt = 0;
47516
47756
  helloTimer = null;
47517
47757
  reconnectTimer = null;
47758
+ _statsCounters = {
47759
+ messagesIn: 0,
47760
+ messagesOut: 0,
47761
+ toolCalls: 0,
47762
+ errors: 0
47763
+ };
47764
+ _sessionStartedAt = Date.now();
47765
+ _statsReportTimer = null;
47518
47766
  constructor(mesh, opts = {}) {
47519
47767
  this.mesh = mesh;
47520
47768
  this.opts = opts;
@@ -47566,6 +47814,7 @@ class BrokerClient {
47566
47814
  sessionId: `${process.pid}-${Date.now()}`,
47567
47815
  pid: process.pid,
47568
47816
  cwd: process.cwd(),
47817
+ hostname: __require("os").hostname(),
47569
47818
  peerType: "ai",
47570
47819
  channel: "claude-code",
47571
47820
  model: process.env.CLAUDE_MODEL || undefined,
@@ -47596,6 +47845,19 @@ class BrokerClient {
47596
47845
  this.setConnStatus("open");
47597
47846
  this.reconnectAttempt = 0;
47598
47847
  this.flushOutbound();
47848
+ this.startStatsReporting();
47849
+ if (msg.restored) {
47850
+ const groups = msg.restoredGroups ? msg.restoredGroups.map((g) => g.role ? `@${g.name}:${g.role}` : `@${g.name}`).join(", ") : "none";
47851
+ process.stderr.write(`[claudemesh] session restored — last seen ${msg.lastSeenAt ?? "unknown"}, groups: ${groups}
47852
+ `);
47853
+ if (msg.restoredStats) {
47854
+ const rs = msg.restoredStats;
47855
+ this._statsCounters.messagesIn = rs.messagesIn ?? 0;
47856
+ this._statsCounters.messagesOut = rs.messagesOut ?? 0;
47857
+ this._statsCounters.toolCalls = rs.toolCalls ?? 0;
47858
+ this._statsCounters.errors = rs.errors ?? 0;
47859
+ }
47860
+ }
47599
47861
  resolve();
47600
47862
  return;
47601
47863
  }
@@ -47635,6 +47897,7 @@ class BrokerClient {
47635
47897
  nonce = randomNonce();
47636
47898
  ciphertext = Buffer.from(message, "utf-8").toString("base64");
47637
47899
  }
47900
+ this._statsCounters.messagesOut++;
47638
47901
  return new Promise((resolve) => {
47639
47902
  if (this.pendingSends.size >= MAX_QUEUED) {
47640
47903
  resolve({ ok: false, error: "outbound queue full" });
@@ -47692,6 +47955,16 @@ class BrokerClient {
47692
47955
  return;
47693
47956
  this.ws.send(JSON.stringify({ type: "set_status", status }));
47694
47957
  }
47958
+ async setVisible(visible) {
47959
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47960
+ return;
47961
+ this.ws.send(JSON.stringify({ type: "set_visible", visible }));
47962
+ }
47963
+ async setProfile(profile) {
47964
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47965
+ return;
47966
+ this.ws.send(JSON.stringify({ type: "set_profile", ...profile }));
47967
+ }
47695
47968
  async listPeers() {
47696
47969
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47697
47970
  return [];
@@ -47709,6 +47982,34 @@ class BrokerClient {
47709
47982
  return;
47710
47983
  this.ws.send(JSON.stringify({ type: "set_summary", summary }));
47711
47984
  }
47985
+ setStats(stats) {
47986
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47987
+ return;
47988
+ const payload = stats ?? {
47989
+ ...this._statsCounters,
47990
+ uptime: Math.round((Date.now() - this._sessionStartedAt) / 1000)
47991
+ };
47992
+ this.ws.send(JSON.stringify({ type: "set_stats", stats: payload }));
47993
+ }
47994
+ incrementToolCalls() {
47995
+ this._statsCounters.toolCalls++;
47996
+ }
47997
+ incrementErrors() {
47998
+ this._statsCounters.errors++;
47999
+ }
48000
+ startStatsReporting() {
48001
+ if (this._statsReportTimer)
48002
+ return;
48003
+ this._statsReportTimer = setInterval(() => {
48004
+ this.setStats();
48005
+ }, 60000);
48006
+ }
48007
+ stopStatsReporting() {
48008
+ if (this._statsReportTimer) {
48009
+ clearInterval(this._statsReportTimer);
48010
+ this._statsReportTimer = null;
48011
+ }
48012
+ }
47712
48013
  async joinGroup(name, role) {
47713
48014
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47714
48015
  return;
@@ -47841,6 +48142,10 @@ class BrokerClient {
47841
48142
  scheduledAckResolvers = new Map;
47842
48143
  scheduledListResolvers = new Map;
47843
48144
  cancelScheduledResolvers = new Map;
48145
+ mcpRegisterResolvers = new Map;
48146
+ mcpListResolvers = new Map;
48147
+ mcpCallResolvers = new Map;
48148
+ mcpCallForwardHandler = null;
47844
48149
  async messageStatus(messageId) {
47845
48150
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
47846
48151
  return null;
@@ -48139,7 +48444,105 @@ class BrokerClient {
48139
48444
  this.stateChangeHandlers.add(handler);
48140
48445
  return () => this.stateChangeHandlers.delete(handler);
48141
48446
  }
48447
+ async mcpRegister(serverName, description, tools, persistent) {
48448
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48449
+ return null;
48450
+ return new Promise((resolve) => {
48451
+ const reqId = this.makeReqId();
48452
+ this.mcpRegisterResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48453
+ if (this.mcpRegisterResolvers.delete(reqId))
48454
+ resolve(null);
48455
+ }, 5000) });
48456
+ this.ws.send(JSON.stringify({ type: "mcp_register", serverName, description, tools, ...persistent ? { persistent: true } : {}, _reqId: reqId }));
48457
+ });
48458
+ }
48459
+ async mcpUnregister(serverName) {
48460
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48461
+ return;
48462
+ this.ws.send(JSON.stringify({ type: "mcp_unregister", serverName }));
48463
+ }
48464
+ async mcpList() {
48465
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48466
+ return [];
48467
+ return new Promise((resolve) => {
48468
+ const reqId = this.makeReqId();
48469
+ this.mcpListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48470
+ if (this.mcpListResolvers.delete(reqId))
48471
+ resolve([]);
48472
+ }, 5000) });
48473
+ this.ws.send(JSON.stringify({ type: "mcp_list", _reqId: reqId }));
48474
+ });
48475
+ }
48476
+ async mcpCall(serverName, toolName, args) {
48477
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48478
+ return { error: "not connected" };
48479
+ return new Promise((resolve) => {
48480
+ const reqId = this.makeReqId();
48481
+ this.mcpCallResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48482
+ if (this.mcpCallResolvers.delete(reqId))
48483
+ resolve({ error: "MCP call timed out (30s)" });
48484
+ }, 30000) });
48485
+ this.ws.send(JSON.stringify({ type: "mcp_call", serverName, toolName, args, _reqId: reqId }));
48486
+ });
48487
+ }
48488
+ onMcpCallForward(handler) {
48489
+ this.mcpCallForwardHandler = handler;
48490
+ }
48491
+ sendMcpCallResponse(callId, result, error2) {
48492
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48493
+ return;
48494
+ this.ws.send(JSON.stringify({ type: "mcp_call_response", callId, result, error: error2 }));
48495
+ }
48142
48496
  meshInfoResolvers = new Map;
48497
+ clockStatusResolvers = new Map;
48498
+ async setClock(speed) {
48499
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48500
+ return null;
48501
+ return new Promise((resolve) => {
48502
+ const reqId = this.makeReqId();
48503
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48504
+ if (this.clockStatusResolvers.delete(reqId))
48505
+ resolve(null);
48506
+ }, 5000) });
48507
+ this.ws.send(JSON.stringify({ type: "set_clock", speed, _reqId: reqId }));
48508
+ });
48509
+ }
48510
+ async pauseClock() {
48511
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48512
+ return null;
48513
+ return new Promise((resolve) => {
48514
+ const reqId = this.makeReqId();
48515
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48516
+ if (this.clockStatusResolvers.delete(reqId))
48517
+ resolve(null);
48518
+ }, 5000) });
48519
+ this.ws.send(JSON.stringify({ type: "pause_clock", _reqId: reqId }));
48520
+ });
48521
+ }
48522
+ async resumeClock() {
48523
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48524
+ return null;
48525
+ return new Promise((resolve) => {
48526
+ const reqId = this.makeReqId();
48527
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48528
+ if (this.clockStatusResolvers.delete(reqId))
48529
+ resolve(null);
48530
+ }, 5000) });
48531
+ this.ws.send(JSON.stringify({ type: "resume_clock", _reqId: reqId }));
48532
+ });
48533
+ }
48534
+ async getClock() {
48535
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48536
+ return null;
48537
+ return new Promise((resolve) => {
48538
+ const reqId = this.makeReqId();
48539
+ this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48540
+ if (this.clockStatusResolvers.delete(reqId))
48541
+ resolve(null);
48542
+ }, 5000) });
48543
+ this.ws.send(JSON.stringify({ type: "get_clock", _reqId: reqId }));
48544
+ });
48545
+ }
48143
48546
  async meshInfo() {
48144
48547
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48145
48548
  return null;
@@ -48152,8 +48555,132 @@ class BrokerClient {
48152
48555
  this.ws.send(JSON.stringify({ type: "mesh_info", _reqId: reqId }));
48153
48556
  });
48154
48557
  }
48558
+ skillAckResolvers = new Map;
48559
+ skillDataResolvers = new Map;
48560
+ skillListResolvers = new Map;
48561
+ async shareSkill(name, description, instructions, tags) {
48562
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48563
+ return null;
48564
+ return new Promise((resolve) => {
48565
+ const reqId = this.makeReqId();
48566
+ this.skillAckResolvers.set(reqId, { resolve: (result) => {
48567
+ resolve(result ? { ok: true, action: result.action } : null);
48568
+ }, timer: setTimeout(() => {
48569
+ if (this.skillAckResolvers.delete(reqId))
48570
+ resolve(null);
48571
+ }, 5000) });
48572
+ this.ws.send(JSON.stringify({ type: "share_skill", name, description, instructions, tags, _reqId: reqId }));
48573
+ });
48574
+ }
48575
+ async getSkill(name) {
48576
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48577
+ return null;
48578
+ return new Promise((resolve) => {
48579
+ const reqId = this.makeReqId();
48580
+ this.skillDataResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48581
+ if (this.skillDataResolvers.delete(reqId))
48582
+ resolve(null);
48583
+ }, 5000) });
48584
+ this.ws.send(JSON.stringify({ type: "get_skill", name, _reqId: reqId }));
48585
+ });
48586
+ }
48587
+ async listSkills(query) {
48588
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48589
+ return [];
48590
+ return new Promise((resolve) => {
48591
+ const reqId = this.makeReqId();
48592
+ this.skillListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48593
+ if (this.skillListResolvers.delete(reqId))
48594
+ resolve([]);
48595
+ }, 5000) });
48596
+ this.ws.send(JSON.stringify({ type: "list_skills", query, _reqId: reqId }));
48597
+ });
48598
+ }
48599
+ async removeSkill(name) {
48600
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48601
+ return false;
48602
+ return new Promise((resolve) => {
48603
+ const reqId = this.makeReqId();
48604
+ this.skillAckResolvers.set(reqId, { resolve: (result) => {
48605
+ resolve(result?.action === "removed");
48606
+ }, timer: setTimeout(() => {
48607
+ if (this.skillAckResolvers.delete(reqId))
48608
+ resolve(false);
48609
+ }, 5000) });
48610
+ this.ws.send(JSON.stringify({ type: "remove_skill", name, _reqId: reqId }));
48611
+ });
48612
+ }
48613
+ webhookAckResolvers = new Map;
48614
+ webhookListResolvers = new Map;
48615
+ async createWebhook(name) {
48616
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48617
+ return null;
48618
+ return new Promise((resolve) => {
48619
+ const reqId = this.makeReqId();
48620
+ this.webhookAckResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48621
+ if (this.webhookAckResolvers.delete(reqId))
48622
+ resolve(null);
48623
+ }, 5000) });
48624
+ this.ws.send(JSON.stringify({ type: "create_webhook", name, _reqId: reqId }));
48625
+ });
48626
+ }
48627
+ async listWebhooks() {
48628
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48629
+ return [];
48630
+ return new Promise((resolve) => {
48631
+ const reqId = this.makeReqId();
48632
+ this.webhookListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48633
+ if (this.webhookListResolvers.delete(reqId))
48634
+ resolve([]);
48635
+ }, 5000) });
48636
+ this.ws.send(JSON.stringify({ type: "list_webhooks", _reqId: reqId }));
48637
+ });
48638
+ }
48639
+ async deleteWebhook(name) {
48640
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48641
+ return false;
48642
+ return new Promise((resolve) => {
48643
+ const reqId = this.makeReqId();
48644
+ this.webhookAckResolvers.set(reqId, { resolve: () => resolve(true), timer: setTimeout(() => {
48645
+ if (this.webhookAckResolvers.delete(reqId))
48646
+ resolve(false);
48647
+ }, 5000) });
48648
+ this.ws.send(JSON.stringify({ type: "delete_webhook", name, _reqId: reqId }));
48649
+ });
48650
+ }
48651
+ setSharedDirs(dirs) {
48652
+ this.sharedDirs = dirs.map((d) => {
48653
+ const { resolve } = __require("node:path");
48654
+ return resolve(d);
48655
+ });
48656
+ }
48657
+ async requestFile(targetPubkey, filePath) {
48658
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48659
+ return { error: "not connected" };
48660
+ return new Promise((resolve) => {
48661
+ const reqId = this.makeReqId();
48662
+ this.peerFileResponseResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48663
+ if (this.peerFileResponseResolvers.delete(reqId))
48664
+ resolve({ error: "timeout waiting for peer response" });
48665
+ }, 15000) });
48666
+ this.ws.send(JSON.stringify({ type: "peer_file_request", targetPubkey, filePath, _reqId: reqId }));
48667
+ });
48668
+ }
48669
+ async requestDir(targetPubkey, dirPath, pattern) {
48670
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48671
+ return { error: "not connected" };
48672
+ return new Promise((resolve) => {
48673
+ const reqId = this.makeReqId();
48674
+ this.peerDirResponseResolvers.set(reqId, { resolve, timer: setTimeout(() => {
48675
+ if (this.peerDirResponseResolvers.delete(reqId))
48676
+ resolve({ error: "timeout waiting for peer response" });
48677
+ }, 15000) });
48678
+ this.ws.send(JSON.stringify({ type: "peer_dir_request", targetPubkey, dirPath, ...pattern ? { pattern } : {}, _reqId: reqId }));
48679
+ });
48680
+ }
48155
48681
  close() {
48156
48682
  this.closed = true;
48683
+ this.stopStatsReporting();
48157
48684
  if (this.helloTimer)
48158
48685
  clearTimeout(this.helloTimer);
48159
48686
  if (this.reconnectTimer)
@@ -48165,6 +48692,141 @@ class BrokerClient {
48165
48692
  }
48166
48693
  this.setConnStatus("closed");
48167
48694
  }
48695
+ static MAX_FILE_SIZE = 1048576;
48696
+ async handlePeerFileRequest(msg) {
48697
+ const { resolve, join: join2, normalize } = await import("node:path");
48698
+ const { readFileSync: readFileSync2, statSync } = await import("node:fs");
48699
+ const reqId = msg._reqId;
48700
+ const sendResponse = (content, error2) => {
48701
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48702
+ return;
48703
+ this.ws.send(JSON.stringify({
48704
+ type: "peer_file_response",
48705
+ requesterPubkey: msg.requesterPubkey,
48706
+ filePath: msg.filePath,
48707
+ ...content !== undefined ? { content } : {},
48708
+ ...error2 ? { error: error2 } : {},
48709
+ ...reqId ? { _reqId: reqId } : {}
48710
+ }));
48711
+ };
48712
+ if (msg.filePath.includes("..")) {
48713
+ sendResponse(undefined, "path traversal not allowed");
48714
+ return;
48715
+ }
48716
+ let resolvedPath = null;
48717
+ for (const dir of this.sharedDirs) {
48718
+ const candidate = resolve(join2(dir, msg.filePath));
48719
+ const normalizedCandidate = normalize(candidate);
48720
+ const normalizedDir = normalize(dir);
48721
+ if (normalizedCandidate.startsWith(normalizedDir + "/") || normalizedCandidate === normalizedDir) {
48722
+ resolvedPath = candidate;
48723
+ break;
48724
+ }
48725
+ }
48726
+ if (!resolvedPath) {
48727
+ sendResponse(undefined, "file outside shared directories");
48728
+ return;
48729
+ }
48730
+ try {
48731
+ const stat = statSync(resolvedPath);
48732
+ if (!stat.isFile()) {
48733
+ sendResponse(undefined, "not a file");
48734
+ return;
48735
+ }
48736
+ if (stat.size > BrokerClient.MAX_FILE_SIZE) {
48737
+ sendResponse(undefined, `file too large (${stat.size} bytes, max ${BrokerClient.MAX_FILE_SIZE})`);
48738
+ return;
48739
+ }
48740
+ const content = readFileSync2(resolvedPath);
48741
+ sendResponse(content.toString("base64"));
48742
+ } catch (e) {
48743
+ const errMsg = e instanceof Error ? e.message : String(e);
48744
+ if (errMsg.includes("ENOENT")) {
48745
+ sendResponse(undefined, "file not found");
48746
+ } else {
48747
+ sendResponse(undefined, `read error: ${errMsg}`);
48748
+ }
48749
+ }
48750
+ }
48751
+ async handlePeerDirRequest(msg) {
48752
+ const { resolve, join: join2, normalize, relative } = await import("node:path");
48753
+ const { readdirSync, statSync } = await import("node:fs");
48754
+ const reqId = msg._reqId;
48755
+ const sendResponse = (entries, error2) => {
48756
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN)
48757
+ return;
48758
+ this.ws.send(JSON.stringify({
48759
+ type: "peer_dir_response",
48760
+ requesterPubkey: msg.requesterPubkey,
48761
+ dirPath: msg.dirPath,
48762
+ ...entries ? { entries } : {},
48763
+ ...error2 ? { error: error2 } : {},
48764
+ ...reqId ? { _reqId: reqId } : {}
48765
+ }));
48766
+ };
48767
+ const dirPath = msg.dirPath || ".";
48768
+ if (dirPath.includes("..")) {
48769
+ sendResponse(undefined, "path traversal not allowed");
48770
+ return;
48771
+ }
48772
+ let resolvedPath = null;
48773
+ for (const dir of this.sharedDirs) {
48774
+ const candidate = resolve(join2(dir, dirPath));
48775
+ const normalizedCandidate = normalize(candidate);
48776
+ const normalizedDir = normalize(dir);
48777
+ if (normalizedCandidate.startsWith(normalizedDir + "/") || normalizedCandidate === normalizedDir) {
48778
+ resolvedPath = candidate;
48779
+ break;
48780
+ }
48781
+ }
48782
+ if (!resolvedPath) {
48783
+ sendResponse(undefined, "directory outside shared directories");
48784
+ return;
48785
+ }
48786
+ try {
48787
+ const stat = statSync(resolvedPath);
48788
+ if (!stat.isDirectory()) {
48789
+ sendResponse(undefined, "not a directory");
48790
+ return;
48791
+ }
48792
+ const entries = [];
48793
+ const MAX_ENTRIES = 500;
48794
+ const MAX_DEPTH = 2;
48795
+ const pattern = msg.pattern ? new RegExp(msg.pattern.replace(/\*/g, ".*").replace(/\?/g, "."), "i") : null;
48796
+ const walk = (dir, depth) => {
48797
+ if (entries.length >= MAX_ENTRIES || depth > MAX_DEPTH)
48798
+ return;
48799
+ try {
48800
+ const items = readdirSync(dir, { withFileTypes: true });
48801
+ for (const item of items) {
48802
+ if (entries.length >= MAX_ENTRIES)
48803
+ break;
48804
+ if (item.name.startsWith("."))
48805
+ continue;
48806
+ const relPath = relative(resolvedPath, join2(dir, item.name));
48807
+ const label = item.isDirectory() ? relPath + "/" : relPath;
48808
+ if (pattern && !pattern.test(item.name)) {
48809
+ if (item.isDirectory())
48810
+ walk(join2(dir, item.name), depth + 1);
48811
+ continue;
48812
+ }
48813
+ entries.push(label);
48814
+ if (item.isDirectory())
48815
+ walk(join2(dir, item.name), depth + 1);
48816
+ }
48817
+ } catch {}
48818
+ };
48819
+ walk(resolvedPath, 0);
48820
+ sendResponse(entries.sort());
48821
+ } catch (e) {
48822
+ const errMsg = e instanceof Error ? e.message : String(e);
48823
+ if (errMsg.includes("ENOENT")) {
48824
+ sendResponse(undefined, "directory not found");
48825
+ } else {
48826
+ sendResponse(undefined, `read error: ${errMsg}`);
48827
+ }
48828
+ }
48829
+ }
48168
48830
  resolveFromMap(map2, reqId, value) {
48169
48831
  let entry = reqId ? map2.get(reqId) : undefined;
48170
48832
  if (!entry) {
@@ -48202,6 +48864,7 @@ class BrokerClient {
48202
48864
  return;
48203
48865
  }
48204
48866
  if (msg.type === "push") {
48867
+ this._statsCounters.messagesIn++;
48205
48868
  const nonce = String(msg.nonce ?? "");
48206
48869
  const ciphertext = String(msg.ciphertext ?? "");
48207
48870
  const senderPubkey = String(msg.senderPubkey ?? "");
@@ -48402,10 +49065,34 @@ class BrokerClient {
48402
49065
  }
48403
49066
  return;
48404
49067
  }
49068
+ if (msg.type === "clock_status") {
49069
+ this.resolveFromMap(this.clockStatusResolvers, msgReqId, {
49070
+ speed: Number(msg.speed ?? 0),
49071
+ paused: Boolean(msg.paused),
49072
+ tick: Number(msg.tick ?? 0),
49073
+ simTime: String(msg.simTime ?? ""),
49074
+ startedAt: String(msg.startedAt ?? "")
49075
+ });
49076
+ return;
49077
+ }
48405
49078
  if (msg.type === "mesh_info_result") {
48406
49079
  this.resolveFromMap(this.meshInfoResolvers, msgReqId, msg);
48407
49080
  return;
48408
49081
  }
49082
+ if (msg.type === "skill_ack") {
49083
+ this.resolveFromMap(this.skillAckResolvers, msgReqId, { name: String(msg.name ?? ""), action: String(msg.action ?? "") });
49084
+ return;
49085
+ }
49086
+ if (msg.type === "skill_data") {
49087
+ const skill = msg.skill;
49088
+ this.resolveFromMap(this.skillDataResolvers, msgReqId, skill ?? null);
49089
+ return;
49090
+ }
49091
+ if (msg.type === "skill_list") {
49092
+ const skills = msg.skills ?? [];
49093
+ this.resolveFromMap(this.skillListResolvers, msgReqId, skills);
49094
+ return;
49095
+ }
48409
49096
  if (msg.type === "scheduled_ack") {
48410
49097
  this.resolveFromMap(this.scheduledAckResolvers, msgReqId, {
48411
49098
  scheduledId: String(msg.scheduledId ?? ""),
@@ -48423,6 +49110,75 @@ class BrokerClient {
48423
49110
  this.resolveFromMap(this.cancelScheduledResolvers, msgReqId, Boolean(msg.ok));
48424
49111
  return;
48425
49112
  }
49113
+ if (msg.type === "mcp_register_ack") {
49114
+ this.resolveFromMap(this.mcpRegisterResolvers, msgReqId, {
49115
+ serverName: String(msg.serverName ?? ""),
49116
+ toolCount: Number(msg.toolCount ?? 0)
49117
+ });
49118
+ return;
49119
+ }
49120
+ if (msg.type === "mcp_list_result") {
49121
+ const servers = msg.servers ?? [];
49122
+ this.resolveFromMap(this.mcpListResolvers, msgReqId, servers);
49123
+ return;
49124
+ }
49125
+ if (msg.type === "mcp_call_result") {
49126
+ this.resolveFromMap(this.mcpCallResolvers, msgReqId, {
49127
+ ...msg.result !== undefined ? { result: msg.result } : {},
49128
+ ...msg.error ? { error: String(msg.error) } : {}
49129
+ });
49130
+ return;
49131
+ }
49132
+ if (msg.type === "mcp_call_forward") {
49133
+ const forward = {
49134
+ callId: String(msg.callId ?? ""),
49135
+ serverName: String(msg.serverName ?? ""),
49136
+ toolName: String(msg.toolName ?? ""),
49137
+ args: msg.args ?? {},
49138
+ callerName: String(msg.callerName ?? "")
49139
+ };
49140
+ if (this.mcpCallForwardHandler) {
49141
+ 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)));
49142
+ } else {
49143
+ this.sendMcpCallResponse(forward.callId, undefined, "No MCP call handler registered on this peer");
49144
+ }
49145
+ return;
49146
+ }
49147
+ if (msg.type === "peer_file_request_forward") {
49148
+ this.handlePeerFileRequest(msg);
49149
+ return;
49150
+ }
49151
+ if (msg.type === "peer_file_response_forward") {
49152
+ this.resolveFromMap(this.peerFileResponseResolvers, msgReqId, {
49153
+ content: msg.content ? String(msg.content) : undefined,
49154
+ error: msg.error ? String(msg.error) : undefined
49155
+ });
49156
+ return;
49157
+ }
49158
+ if (msg.type === "peer_dir_request_forward") {
49159
+ this.handlePeerDirRequest(msg);
49160
+ return;
49161
+ }
49162
+ if (msg.type === "peer_dir_response_forward") {
49163
+ this.resolveFromMap(this.peerDirResponseResolvers, msgReqId, {
49164
+ entries: msg.entries ?? undefined,
49165
+ error: msg.error ? String(msg.error) : undefined
49166
+ });
49167
+ return;
49168
+ }
49169
+ if (msg.type === "webhook_ack") {
49170
+ this.resolveFromMap(this.webhookAckResolvers, msgReqId, {
49171
+ name: String(msg.name ?? ""),
49172
+ url: String(msg.url ?? ""),
49173
+ secret: String(msg.secret ?? "")
49174
+ });
49175
+ return;
49176
+ }
49177
+ if (msg.type === "webhook_list") {
49178
+ const webhooks = msg.webhooks ?? [];
49179
+ this.resolveFromMap(this.webhookListResolvers, msgReqId, webhooks);
49180
+ return;
49181
+ }
48426
49182
  if (msg.type === "error") {
48427
49183
  this.debug(`broker error: ${msg.code} ${msg.message}`);
48428
49184
  const id = msg.id ? String(msg.id) : null;
@@ -48465,7 +49221,18 @@ class BrokerClient {
48465
49221
  [this.taskCreatedResolvers, null],
48466
49222
  [this.streamCreatedResolvers, null],
48467
49223
  [this.listPeersResolvers, []],
48468
- [this.meshInfoResolvers, null]
49224
+ [this.meshInfoResolvers, null],
49225
+ [this.clockStatusResolvers, null],
49226
+ [this.mcpRegisterResolvers, null],
49227
+ [this.mcpListResolvers, []],
49228
+ [this.mcpCallResolvers, { error: "broker error" }],
49229
+ [this.skillAckResolvers, null],
49230
+ [this.skillDataResolvers, null],
49231
+ [this.skillListResolvers, []],
49232
+ [this.peerFileResponseResolvers, { error: "broker error" }],
49233
+ [this.peerDirResponseResolvers, { error: "broker error" }],
49234
+ [this.webhookAckResolvers, null],
49235
+ [this.webhookListResolvers, []]
48469
49236
  ];
48470
49237
  for (const [map2, defaultVal] of allMaps) {
48471
49238
  const first = map2.entries().next().value;
@@ -48562,6 +49329,25 @@ function stopAll() {
48562
49329
  }
48563
49330
 
48564
49331
  // src/mcp/server.ts
49332
+ function relativeTime(isoStr) {
49333
+ const then = new Date(isoStr).getTime();
49334
+ if (isNaN(then))
49335
+ return "unknown";
49336
+ const diffMs = Date.now() - then;
49337
+ if (diffMs < 0)
49338
+ return "just now";
49339
+ const seconds = Math.floor(diffMs / 1000);
49340
+ if (seconds < 60)
49341
+ return `${seconds}s ago`;
49342
+ const minutes = Math.floor(seconds / 60);
49343
+ if (minutes < 60)
49344
+ return `${minutes}m ago`;
49345
+ const hours = Math.floor(minutes / 60);
49346
+ if (hours < 24)
49347
+ return `${hours}h ago`;
49348
+ const days = Math.floor(hours / 24);
49349
+ return `${days} day${days !== 1 ? "s" : ""} ago`;
49350
+ }
48565
49351
  function text(msg, isError = false) {
48566
49352
  return {
48567
49353
  content: [{ type: "text", text: msg }],
@@ -48668,6 +49454,8 @@ If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder
48668
49454
  | check_messages() | Drain buffered inbound messages (auto-pushed in most cases, use as fallback). |
48669
49455
  | set_summary(summary) | Set 1-2 sentence description of your current work, visible to all peers. |
48670
49456
  | set_status(status) | Override status: idle, working, or dnd. |
49457
+ | set_visible(visible) | Toggle visibility. Hidden peers skip list_peers and broadcasts; direct messages still arrive. |
49458
+ | set_profile(avatar?, title?, bio?, capabilities?) | Set public profile: emoji avatar, short title, bio, capabilities list. |
48671
49459
  | join_group(name, role?) | Join a @group with optional role (lead, member, observer, or any string). |
48672
49460
  | leave_group(name) | Leave a @group. |
48673
49461
  | set_state(key, value) | Write shared state; pushes change to all peers. |
@@ -48704,6 +49492,12 @@ If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder
48704
49492
  | 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
49493
  | list_scheduled() | List pending scheduled reminders and messages. |
48706
49494
  | cancel_scheduled(id) | Cancel a pending scheduled item. |
49495
+ | read_peer_file(peer, path) | Read a file from another peer's project (max 1MB). |
49496
+ | list_peer_files(peer, path?, pattern?) | List files in a peer's shared directory. |
49497
+ | mesh_mcp_register(server_name, description, tools) | Register an MCP server with the mesh. Other peers can call its tools. |
49498
+ | mesh_mcp_list() | List MCP servers available in the mesh with their tools. |
49499
+ | mesh_tool_call(server_name, tool_name, args?) | Call a tool on a mesh-registered MCP server (30s timeout). |
49500
+ | mesh_mcp_remove(server_name) | Unregister an MCP server you registered. |
48707
49501
 
48708
49502
  If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
48709
49503
 
@@ -48726,9 +49520,14 @@ Shared key-value store scoped to the mesh. Use get_state/set_state for live coor
48726
49520
  ## Memory
48727
49521
  Persistent knowledge that survives across sessions. Use remember(content, tags?) to store lessons, decisions, and incidents. Use recall(query) to search before asking peers. New peers should recall at session start to load institutional knowledge.
48728
49522
 
48729
- ## Files
48730
- share_file for persistent references, send_message(file:) for ephemeral attachments.
48731
- Tags on shared files make them searchable. Use list_files to find what peers shared.
49523
+ ## File access — decision guide
49524
+ Three ways to access files. Pick the right one:
49525
+
49526
+ 1. **Local peer (same machine, [local] tag):** Read files directly via filesystem using their \`cwd\` path from list_peers. No limit, instant. This is the default for local peers.
49527
+ 2. **Remote peer (different machine, [remote] tag):** Use \`read_peer_file(peer, path)\` — relays through the mesh. **1 MB limit**, base64 encoded. Use \`list_peer_files\` to browse first.
49528
+ 3. **Persistent sharing (any peer):** Use \`share_file(path)\` — uploads to mesh storage (MinIO). **No size limit**. All peers can download anytime via \`get_file\`. Use for files that need to persist or be shared with multiple peers.
49529
+
49530
+ **Rule of thumb:** local peer → filesystem. Remote peer, small file → read_peer_file. Large file or needs to persist → share_file.
48732
49531
 
48733
49532
  ## Vectors
48734
49533
  Store and search semantic embeddings. Use vector_store to index content, vector_search to find similar content.
@@ -48767,6 +49566,9 @@ Your message mode is "${messageMode}".
48767
49566
  }));
48768
49567
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
48769
49568
  const { name, arguments: args } = req.params;
49569
+ for (const c of allClients()) {
49570
+ c.incrementToolCalls();
49571
+ }
48770
49572
  if (config2.meshes.length === 0) {
48771
49573
  return text("No meshes joined. Run `claudemesh join https://claudemesh.com/join/<token>` first.", true);
48772
49574
  }
@@ -48822,7 +49624,12 @@ No peers connected.`);
48822
49624
  meta2.push(`model:${p.model}`);
48823
49625
  const metaStr = meta2.length ? ` {${meta2.join(", ")}}` : "";
48824
49626
  const cwdStr = p.cwd ? ` cwd:${p.cwd}` : "";
48825
- return `- **${p.displayName}** [${p.status}]${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`;
49627
+ const locality = p.hostname && p.hostname === __require("os").hostname() ? "local" : "remote";
49628
+ const localityTag = ` [${locality}]`;
49629
+ const profileAvatar = p.profile?.avatar ? `${p.profile.avatar} ` : "";
49630
+ const profileTitle = p.profile?.title ? ` (${p.profile.title})` : "";
49631
+ const hiddenTag = p.visible === false ? " [hidden]" : "";
49632
+ return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${localityTag}${hiddenTag}${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`;
48826
49633
  });
48827
49634
  sections.push(`${header}
48828
49635
  ${peerLines.join(`
@@ -48889,6 +49696,32 @@ ${drained.join(`
48889
49696
  await c.setStatus(s);
48890
49697
  return text(`Status set to ${s} across ${allClients().length} mesh(es).`);
48891
49698
  }
49699
+ case "set_visible": {
49700
+ const { visible } = args ?? {};
49701
+ if (visible === undefined)
49702
+ return text("set_visible: `visible` required", true);
49703
+ for (const c of allClients())
49704
+ await c.setVisible(visible);
49705
+ 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.");
49706
+ }
49707
+ case "set_profile": {
49708
+ const { avatar, title, bio, capabilities } = args ?? {};
49709
+ const profile = { avatar, title, bio, capabilities };
49710
+ for (const c of allClients())
49711
+ await c.setProfile(profile);
49712
+ const parts = [];
49713
+ if (avatar)
49714
+ parts.push(`Avatar: ${avatar}`);
49715
+ if (title)
49716
+ parts.push(`Title: ${title}`);
49717
+ if (bio)
49718
+ parts.push(`Bio: ${bio}`);
49719
+ if (capabilities?.length)
49720
+ parts.push(`Capabilities: ${capabilities.join(", ")}`);
49721
+ return text(parts.length > 0 ? `Profile updated:
49722
+ ${parts.join(`
49723
+ `)}` : "Profile cleared.");
49724
+ }
48892
49725
  case "join_group": {
48893
49726
  const { name: groupName, role } = args ?? {};
48894
49727
  if (!groupName)
@@ -49422,6 +50255,72 @@ ${rows.join(`
49422
50255
  return text("No active streams.");
49423
50256
  const lines = streams.map((s) => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
49424
50257
  return text(lines.join(`
50258
+ `));
50259
+ }
50260
+ case "mesh_set_clock": {
50261
+ const { speed } = args ?? {};
50262
+ if (!speed || speed < 1 || speed > 100)
50263
+ return text("mesh_set_clock: speed must be 1-100", true);
50264
+ const client2 = allClients()[0];
50265
+ if (!client2)
50266
+ return text("mesh_set_clock: not connected", true);
50267
+ const result = await client2.setClock(speed);
50268
+ if (!result)
50269
+ return text("mesh_set_clock: timed out", true);
50270
+ return text([
50271
+ `**Clock set to x${result.speed}**`,
50272
+ `Paused: ${result.paused}`,
50273
+ `Tick: ${result.tick}`,
50274
+ `Sim time: ${result.simTime}`,
50275
+ `Started at: ${result.startedAt}`
50276
+ ].join(`
50277
+ `));
50278
+ }
50279
+ case "mesh_pause_clock": {
50280
+ const client2 = allClients()[0];
50281
+ if (!client2)
50282
+ return text("mesh_pause_clock: not connected", true);
50283
+ const result = await client2.pauseClock();
50284
+ if (!result)
50285
+ return text("mesh_pause_clock: timed out", true);
50286
+ return text([
50287
+ "**Clock paused**",
50288
+ `Speed: x${result.speed}`,
50289
+ `Tick: ${result.tick}`,
50290
+ `Sim time: ${result.simTime}`
50291
+ ].join(`
50292
+ `));
50293
+ }
50294
+ case "mesh_resume_clock": {
50295
+ const client2 = allClients()[0];
50296
+ if (!client2)
50297
+ return text("mesh_resume_clock: not connected", true);
50298
+ const result = await client2.resumeClock();
50299
+ if (!result)
50300
+ return text("mesh_resume_clock: timed out", true);
50301
+ return text([
50302
+ "**Clock resumed**",
50303
+ `Speed: x${result.speed}`,
50304
+ `Tick: ${result.tick}`,
50305
+ `Sim time: ${result.simTime}`
50306
+ ].join(`
50307
+ `));
50308
+ }
50309
+ case "mesh_clock": {
50310
+ const client2 = allClients()[0];
50311
+ if (!client2)
50312
+ return text("mesh_clock: not connected", true);
50313
+ const result = await client2.getClock();
50314
+ if (!result)
50315
+ return text("mesh_clock: timed out", true);
50316
+ const statusLabel = result.speed === 0 ? "not started" : result.paused ? "paused" : "running";
50317
+ return text([
50318
+ `**Clock status: ${statusLabel}**`,
50319
+ `Speed: x${result.speed}`,
50320
+ `Tick: ${result.tick}`,
50321
+ `Sim time: ${result.simTime}`,
50322
+ `Started at: ${result.startedAt}`
50323
+ ].join(`
49425
50324
  `));
49426
50325
  }
49427
50326
  case "mesh_info": {
@@ -49447,6 +50346,89 @@ ${rows.join(`
49447
50346
  return text(lines.join(`
49448
50347
  `));
49449
50348
  }
50349
+ case "mesh_stats": {
50350
+ const clients2 = allClients();
50351
+ if (clients2.length === 0)
50352
+ return text("mesh_stats: no joined meshes", true);
50353
+ const sections = [];
50354
+ for (const c of clients2) {
50355
+ const peers = await c.listPeers();
50356
+ const header = `## ${c.meshSlug}`;
50357
+ const rows = peers.map((p) => {
50358
+ const s = p.stats;
50359
+ if (!s)
50360
+ return `| ${p.displayName} | - | - | - | - | - |`;
50361
+ const up = s.uptime != null ? `${Math.floor(s.uptime / 60)}m` : "-";
50362
+ return `| ${p.displayName} | ${s.messagesIn ?? 0} | ${s.messagesOut ?? 0} | ${s.toolCalls ?? 0} | ${up} | ${s.errors ?? 0} |`;
50363
+ });
50364
+ sections.push(`${header}
50365
+ | Peer | Msgs In | Msgs Out | Tool Calls | Uptime | Errors |
50366
+ |------|---------|----------|------------|--------|--------|
50367
+ ${rows.join(`
50368
+ `)}`);
50369
+ }
50370
+ return text(sections.join(`
50371
+
50372
+ `));
50373
+ }
50374
+ case "share_skill": {
50375
+ const { name: skillName, description: skillDesc, instructions: skillInstr, tags: skillTags } = args ?? {};
50376
+ if (!skillName || !skillDesc || !skillInstr)
50377
+ return text("share_skill: `name`, `description`, and `instructions` required", true);
50378
+ const client2 = allClients()[0];
50379
+ if (!client2)
50380
+ return text("share_skill: not connected", true);
50381
+ const result = await client2.shareSkill(skillName, skillDesc, skillInstr, skillTags);
50382
+ if (!result)
50383
+ return text("share_skill: broker did not acknowledge", true);
50384
+ return text(`Skill "${skillName}" published to the mesh.`);
50385
+ }
50386
+ case "get_skill": {
50387
+ const { name: gsName } = args ?? {};
50388
+ if (!gsName)
50389
+ return text("get_skill: `name` required", true);
50390
+ const client2 = allClients()[0];
50391
+ if (!client2)
50392
+ return text("get_skill: not connected", true);
50393
+ const skill = await client2.getSkill(gsName);
50394
+ if (!skill)
50395
+ return text(`Skill "${gsName}" not found in the mesh.`);
50396
+ return text(`# Skill: ${skill.name}
50397
+
50398
+ **Description:** ${skill.description}
50399
+ **Author:** ${skill.author}
50400
+ **Tags:** ${skill.tags.length ? skill.tags.join(", ") : "none"}
50401
+ **Created:** ${skill.createdAt}
50402
+
50403
+ ---
50404
+
50405
+ ## Instructions
50406
+
50407
+ ${skill.instructions}`);
50408
+ }
50409
+ case "list_skills": {
50410
+ const { query: skillQuery } = args ?? {};
50411
+ const client2 = allClients()[0];
50412
+ if (!client2)
50413
+ return text("list_skills: not connected", true);
50414
+ const skills = await client2.listSkills(skillQuery);
50415
+ if (skills.length === 0)
50416
+ return text(skillQuery ? `No skills found for "${skillQuery}".` : "No skills in the mesh yet.");
50417
+ const lines = skills.map((s) => `- **${s.name}**: ${s.description}${s.tags.length ? ` [${s.tags.join(", ")}]` : ""} (by ${s.author})`);
50418
+ return text(`${skills.length} skill(s):
50419
+ ${lines.join(`
50420
+ `)}`);
50421
+ }
50422
+ case "remove_skill": {
50423
+ const { name: rsName } = args ?? {};
50424
+ if (!rsName)
50425
+ return text("remove_skill: `name` required", true);
50426
+ const client2 = allClients()[0];
50427
+ if (!client2)
50428
+ return text("remove_skill: not connected", true);
50429
+ const removed = await client2.removeSkill(rsName);
50430
+ return text(removed ? `Skill "${rsName}" removed.` : `Skill "${rsName}" not found.`, !removed);
50431
+ }
49450
50432
  case "ping_mesh": {
49451
50433
  const { priorities: pingPriorities } = args ?? {};
49452
50434
  const toTest = pingPriorities ?? ["now", "next"];
@@ -49483,6 +50465,59 @@ ${rows.join(`
49483
50465
  return text(results.join(`
49484
50466
  `));
49485
50467
  }
50468
+ case "mesh_mcp_register": {
50469
+ const { server_name, description, tools: regTools, persistent: regPersistent } = args ?? {};
50470
+ if (!server_name || !description || !regTools?.length)
50471
+ return text("mesh_mcp_register: `server_name`, `description`, and `tools` required", true);
50472
+ const client2 = allClients()[0];
50473
+ if (!client2)
50474
+ return text("mesh_mcp_register: not connected", true);
50475
+ const result = await client2.mcpRegister(server_name, description, regTools, regPersistent);
50476
+ if (!result)
50477
+ return text("mesh_mcp_register: broker did not acknowledge", true);
50478
+ const persistLabel = regPersistent ? " (persistent — survives disconnect)" : "";
50479
+ return text(`Registered MCP server "${result.serverName}" with ${result.toolCount} tool(s)${persistLabel}. Other peers can now call its tools via mesh_tool_call.`);
50480
+ }
50481
+ case "mesh_mcp_list": {
50482
+ const client2 = allClients()[0];
50483
+ if (!client2)
50484
+ return text("mesh_mcp_list: not connected", true);
50485
+ const servers = await client2.mcpList();
50486
+ if (servers.length === 0)
50487
+ return text("No MCP servers registered in the mesh.");
50488
+ const lines = servers.map((s) => {
50489
+ const toolList = s.tools.map((t) => ` - **${t.name}**: ${t.description}`).join(`
50490
+ `);
50491
+ const status = s.online === false ? ` [OFFLINE${s.offlineSince ? ` since ${s.offlineSince}` : ""}]` : "";
50492
+ return `- **${s.name}** (hosted by ${s.hostedBy})${status}: ${s.description}
50493
+ ${toolList}`;
50494
+ });
50495
+ return text(`${servers.length} MCP server(s) in mesh:
50496
+ ${lines.join(`
50497
+ `)}`);
50498
+ }
50499
+ case "mesh_tool_call": {
50500
+ const { server_name: callServer, tool_name: callTool, args: callArgs } = args ?? {};
50501
+ if (!callServer || !callTool)
50502
+ return text("mesh_tool_call: `server_name` and `tool_name` required", true);
50503
+ const client2 = allClients()[0];
50504
+ if (!client2)
50505
+ return text("mesh_tool_call: not connected", true);
50506
+ const callResult = await client2.mcpCall(callServer, callTool, callArgs ?? {});
50507
+ if (callResult.error)
50508
+ return text(`mesh_tool_call error: ${callResult.error}`, true);
50509
+ return text(typeof callResult.result === "string" ? callResult.result : JSON.stringify(callResult.result, null, 2));
50510
+ }
50511
+ case "mesh_mcp_remove": {
50512
+ const { server_name: rmServer } = args ?? {};
50513
+ if (!rmServer)
50514
+ return text("mesh_mcp_remove: `server_name` required", true);
50515
+ const client2 = allClients()[0];
50516
+ if (!client2)
50517
+ return text("mesh_mcp_remove: not connected", true);
50518
+ await client2.mcpUnregister(rmServer);
50519
+ return text(`Unregistered MCP server "${rmServer}" from the mesh.`);
50520
+ }
49486
50521
  case "grant_file_access": {
49487
50522
  const { fileId, to: grantTo } = args ?? {};
49488
50523
  if (!fileId || !grantTo)
@@ -49515,6 +50550,128 @@ ${rows.join(`
49515
50550
  return text("grant_file_access: broker did not confirm", true);
49516
50551
  return text(`Access granted: ${targetPeer.displayName} can now download file ${fileId}`);
49517
50552
  }
50553
+ case "read_peer_file": {
50554
+ const { peer: peerName, path: filePath } = args ?? {};
50555
+ if (!peerName || !filePath)
50556
+ return text("read_peer_file: `peer` and `path` required", true);
50557
+ const client2 = allClients()[0];
50558
+ if (!client2)
50559
+ return text("read_peer_file: not connected", true);
50560
+ const peers = await client2.listPeers();
50561
+ const nameLower = peerName.toLowerCase();
50562
+ let targetPubkey = null;
50563
+ if (/^[0-9a-f]{64}$/.test(peerName)) {
50564
+ targetPubkey = peerName;
50565
+ } else {
50566
+ const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
50567
+ if (!match) {
50568
+ const partials = peers.filter((p) => p.displayName.toLowerCase().includes(nameLower));
50569
+ if (partials.length === 1) {
50570
+ targetPubkey = partials[0].pubkey;
50571
+ } else {
50572
+ const names = peers.map((p) => p.displayName).join(", ");
50573
+ return text(`read_peer_file: peer "${peerName}" not found. Online: ${names || "(none)"}`, true);
50574
+ }
50575
+ } else {
50576
+ targetPubkey = match.pubkey;
50577
+ }
50578
+ }
50579
+ const resolvedPeer = peers.find((p) => p.pubkey === targetPubkey);
50580
+ const isLocal = resolvedPeer?.hostname && resolvedPeer.hostname === __require("os").hostname();
50581
+ let localHint = "";
50582
+ if (isLocal && resolvedPeer?.cwd) {
50583
+ const directPath = __require("path").resolve(resolvedPeer.cwd, filePath);
50584
+ localHint = `
50585
+
50586
+ > **Hint:** This peer is LOCAL (same machine). Next time, read directly: \`${directPath}\` — faster, no size limit.
50587
+
50588
+ `;
50589
+ }
50590
+ const result = await client2.requestFile(targetPubkey, filePath);
50591
+ if (result.error)
50592
+ return text(`read_peer_file: ${result.error}`, true);
50593
+ if (!result.content)
50594
+ return text("read_peer_file: empty response from peer", true);
50595
+ try {
50596
+ const decoded = Buffer.from(result.content, "base64").toString("utf-8");
50597
+ return text(localHint + decoded);
50598
+ } catch {
50599
+ return text("read_peer_file: failed to decode file content (binary file?)", true);
50600
+ }
50601
+ }
50602
+ case "list_peer_files": {
50603
+ const { peer: peerName, path: dirPath, pattern } = args ?? {};
50604
+ if (!peerName)
50605
+ return text("list_peer_files: `peer` required", true);
50606
+ const client2 = allClients()[0];
50607
+ if (!client2)
50608
+ return text("list_peer_files: not connected", true);
50609
+ const peers = await client2.listPeers();
50610
+ const nameLower = peerName.toLowerCase();
50611
+ let targetPubkey = null;
50612
+ if (/^[0-9a-f]{64}$/.test(peerName)) {
50613
+ targetPubkey = peerName;
50614
+ } else {
50615
+ const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
50616
+ if (!match) {
50617
+ const partials = peers.filter((p) => p.displayName.toLowerCase().includes(nameLower));
50618
+ if (partials.length === 1) {
50619
+ targetPubkey = partials[0].pubkey;
50620
+ } else {
50621
+ const names = peers.map((p) => p.displayName).join(", ");
50622
+ return text(`list_peer_files: peer "${peerName}" not found. Online: ${names || "(none)"}`, true);
50623
+ }
50624
+ } else {
50625
+ targetPubkey = match.pubkey;
50626
+ }
50627
+ }
50628
+ const result = await client2.requestDir(targetPubkey, dirPath ?? ".", pattern);
50629
+ if (result.error)
50630
+ return text(`list_peer_files: ${result.error}`, true);
50631
+ if (!result.entries || result.entries.length === 0)
50632
+ return text("No files found.");
50633
+ return text(result.entries.join(`
50634
+ `));
50635
+ }
50636
+ case "create_webhook": {
50637
+ const { name: whName } = args ?? {};
50638
+ if (!whName)
50639
+ return text("create_webhook: `name` required", true);
50640
+ const client2 = allClients()[0];
50641
+ if (!client2)
50642
+ return text("create_webhook: not connected", true);
50643
+ const wh = await client2.createWebhook(whName);
50644
+ if (!wh)
50645
+ return text("create_webhook: broker did not acknowledge — check connection", true);
50646
+ return text(`Webhook **${wh.name}** created.
50647
+
50648
+ URL: ${wh.url}
50649
+ Secret: ${wh.secret}
50650
+
50651
+ External services can POST JSON to this URL. The payload will be pushed to all connected mesh peers.`);
50652
+ }
50653
+ case "list_webhooks": {
50654
+ const client2 = allClients()[0];
50655
+ if (!client2)
50656
+ return text("list_webhooks: not connected", true);
50657
+ const webhooks = await client2.listWebhooks();
50658
+ if (webhooks.length === 0)
50659
+ return text("No active webhooks.");
50660
+ const lines = webhooks.map((w) => `- **${w.name}** — ${w.url} (created ${w.createdAt})`);
50661
+ return text(`${webhooks.length} webhook(s):
50662
+ ${lines.join(`
50663
+ `)}`);
50664
+ }
50665
+ case "delete_webhook": {
50666
+ const { name: delName } = args ?? {};
50667
+ if (!delName)
50668
+ return text("delete_webhook: `name` required", true);
50669
+ const client2 = allClients()[0];
50670
+ if (!client2)
50671
+ return text("delete_webhook: not connected", true);
50672
+ const ok = await client2.deleteWebhook(delName);
50673
+ return text(ok ? `Webhook "${delName}" deactivated.` : `Failed to deactivate webhook "${delName}".`, !ok);
50674
+ }
49518
50675
  default:
49519
50676
  return text(`Unknown tool: ${name}`, true);
49520
50677
  }
@@ -49530,10 +50687,28 @@ ${rows.join(`
49530
50687
  const eventName = msg.event;
49531
50688
  const data = msg.eventData ?? {};
49532
50689
  let content2;
49533
- if (eventName === "peer_joined") {
50690
+ if (eventName === "tick") {
50691
+ const tick = data.tick ?? 0;
50692
+ const simTime = String(data.simTime ?? "").replace("T", " ").replace(/\..*/, "");
50693
+ const speed = data.speed ?? 1;
50694
+ content2 = `[heartbeat] tick ${tick} | sim time: ${simTime} | speed: x${speed}`;
50695
+ } else if (eventName === "peer_joined") {
49534
50696
  content2 = `[system] Peer "${data.name ?? "unknown"}" joined the mesh`;
50697
+ } else if (eventName === "peer_returned") {
50698
+ const peerName = String(data.name ?? "unknown");
50699
+ const lastSeenAt = data.lastSeenAt ? relativeTime(String(data.lastSeenAt)) : "unknown";
50700
+ const groups = Array.isArray(data.groups) ? data.groups.map((g) => g.role ? `@${g.name}:${g.role}` : `@${g.name}`).join(", ") : "";
50701
+ const summary = data.summary ? ` Summary: "${data.summary}"` : "";
50702
+ content2 = `[system] Welcome back, "${peerName}"! Last seen ${lastSeenAt}.${groups ? ` Restored: ${groups}` : ""}${summary}`;
49535
50703
  } else if (eventName === "peer_left") {
49536
50704
  content2 = `[system] Peer "${data.name ?? "unknown"}" left the mesh`;
50705
+ } else if (eventName === "mcp_registered") {
50706
+ const tools = Array.isArray(data.tools) ? data.tools.join(", ") : "";
50707
+ content2 = `[system] New MCP server available: "${data.serverName}" (hosted by ${data.hostedBy}). Tools: ${tools}. Use mesh_tool_call to invoke.`;
50708
+ } else if (eventName === "mcp_unregistered") {
50709
+ content2 = `[system] MCP server "${data.serverName}" removed (was hosted by ${data.hostedBy})`;
50710
+ } else if (eventName === "mcp_restored") {
50711
+ content2 = `[system] MCP server "${data.serverName}" is back online (hosted by ${data.hostedBy})`;
49537
50712
  } else {
49538
50713
  content2 = `[system] ${eventName}: ${JSON.stringify(data)}`;
49539
50714
  }
@@ -49657,6 +50832,7 @@ ${rows.join(`
49657
50832
  }
49658
50833
 
49659
50834
  // src/commands/install.ts
50835
+ init_config();
49660
50836
  import {
49661
50837
  chmodSync as chmodSync2,
49662
50838
  copyFileSync,
@@ -49957,10 +51133,22 @@ function runInstall(args = []) {
49957
51133
  } else {
49958
51134
  console.log(dim("· Hooks skipped (--no-hooks)"));
49959
51135
  }
51136
+ let hasMeshes = false;
51137
+ try {
51138
+ const meshConfig = loadConfig();
51139
+ hasMeshes = meshConfig.meshes.length > 0;
51140
+ } catch {}
49960
51141
  console.log("");
49961
51142
  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>")}`);
51143
+ if (!hasMeshes) {
51144
+ console.log("");
51145
+ console.log(yellow("No meshes joined.") + " To connect with peers:");
51146
+ console.log(` ${bold2("claudemesh join <invite-url>")}` + dim(" — join an existing mesh"));
51147
+ console.log(` ${dim("Create one at")} ${bold2("https://claudemesh.com/dashboard")}`);
51148
+ } else {
51149
+ console.log("");
51150
+ console.log(`Next: ${bold2("claudemesh join https://claudemesh.com/join/<token>")}`);
51151
+ }
49964
51152
  console.log("");
49965
51153
  console.log(yellow("⚠ For real-time push messages from peers, launch with:"));
49966
51154
  console.log(` ${bold2("claudemesh launch")}` + dim(" (or: claude --dangerously-load-development-channels server:claudemesh)"));
@@ -50620,7 +51808,7 @@ init_config();
50620
51808
  // package.json
50621
51809
  var package_default = {
50622
51810
  name: "claudemesh-cli",
50623
- version: "0.6.9",
51811
+ version: "0.7.1",
50624
51812
  description: "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
50625
51813
  keywords: [
50626
51814
  "claude-code",