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.
- package/dist/index.js +1197 -9
- 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
|
-
##
|
|
48730
|
-
|
|
48731
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
49963
|
-
|
|
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.
|
|
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",
|