Skip to content

Commit 80dfe1e

Browse files
committed
feat: Implement manual P2P peer management commands and enhance bootstrap node handling
1 parent 797d321 commit 80dfe1e

7 files changed

Lines changed: 364 additions & 15 deletions

File tree

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@
3939

4040
---
4141

42+
> [!NOTE]
43+
> **P2P Bootstrap Nodes — Coming Soon**
44+
> Public bootstrap node infrastructure is currently being set up. Until official bootstrap nodes are available, you can connect peers manually:
45+
> ```bash
46+
> # Add a peer's address to your bootstrap list
47+
> infomesh peer add /ip4/<IP>/tcp/4001/p2p/<PEER_ID>
48+
> # Test connectivity to configured bootstrap nodes
49+
> infomesh peer test
50+
> ```
51+
> Or configure directly in `~/.infomesh/config.toml`:
52+
> ```toml
53+
> [network]
54+
> bootstrap_nodes = ["/ip4/<IP>/tcp/4001/p2p/<PEER_ID>"]
55+
> ```
56+
> This will be resolved shortly. Local crawling, indexing, and MCP search work fully without P2P.
57+
58+
---
59+
4260
## 💡 Why InfoMesh?
4361
4462
### The Problem
@@ -232,6 +250,29 @@ Your AI assistant can now search the web for free via MCP.
232250

233251
If you want to contribute code or run from source:
234252

253+
#### System Prerequisites
254+
255+
The P2P optional dependency (`libp2p`) includes C extensions (`fastecdsa`, `coincurve`, `pynacl`) that require native build tools.
256+
257+
**Linux (Debian / Ubuntu):**
258+
259+
```bash
260+
sudo apt-get update && sudo apt-get install -y build-essential python3-dev libgmp-dev
261+
```
262+
263+
**macOS:**
264+
265+
```bash
266+
brew install gmp
267+
# Xcode Command Line Tools are usually pre-installed
268+
```
269+
270+
**Windows:** Use WSL2 (recommended) or install Visual Studio Build Tools + GMP.
271+
272+
> **Note:** These system packages are only required for the `p2p` optional dependency. The base install (`uv sync`) does not need them.
273+
274+
#### Clone & Run
275+
235276
```bash
236277
# Clone and install with dev dependencies
237278
git clone https://github.com/dotnetpower/infomesh.git
@@ -809,6 +850,7 @@ All core phases are **complete**. Current focus is on community growth and produ
809850
### What's Next
810851

811852
- 🌍 **Public bootstrap nodes** — volunteer-run seed nodes for easy onboarding
853+
> ⚠️ **Note**: Public bootstrap node setup is currently in progress. Until official bootstrap nodes are available, you can connect peers manually using `infomesh peer add /ip4/<IP>/tcp/4001/p2p/<PEER_ID>` or configure bootstrap nodes in `~/.infomesh/config.toml`. Use `infomesh peer test` to verify connectivity. This will be resolved shortly.
812854
- 🎭 **JS rendering** — Playwright-based SPA crawling for JS-heavy sites
813855
- 📱 **Web dashboard** — optional browser UI alongside the TUI
814856
- 🔍 **Semantic search fusion** — BM25 + vector hybrid ranking with RRF

infomesh/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def cli() -> None:
4242
from infomesh.cli.crawl import crawl, dashboard, mcp_cmd # noqa: E402
4343
from infomesh.cli.index import index_group # noqa: E402
4444
from infomesh.cli.keys import keys_group # noqa: E402
45+
from infomesh.cli.peer import peer_group # noqa: E402
4546
from infomesh.cli.search import search # noqa: E402
4647
from infomesh.cli.serve import serve, start, status, stop # noqa: E402
4748

@@ -56,3 +57,4 @@ def cli() -> None:
5657
cli.add_command(index_group)
5758
cli.add_command(config_group)
5859
cli.add_command(keys_group)
60+
cli.add_command(peer_group)

infomesh/cli/peer.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""CLI commands: peer add/list/remove — manual P2P peer management."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
7+
import click
8+
9+
from infomesh.config import load_config
10+
11+
12+
@click.group(name="peer")
13+
def peer_group() -> None:
14+
"""Manage P2P peers (add, list, remove bootstrap nodes)."""
15+
16+
17+
@peer_group.command(name="list")
18+
def list_peers() -> None:
19+
"""Show connected peers and bootstrap nodes."""
20+
config = load_config()
21+
22+
# Bootstrap nodes from config
23+
bs_nodes = config.network.bootstrap_nodes
24+
click.echo("Bootstrap nodes:")
25+
if bs_nodes:
26+
for addr in bs_nodes:
27+
click.echo(f" {addr}")
28+
else:
29+
click.echo(" (none configured)")
30+
click.echo(" Add with: infomesh peer add /ip4/<IP>/tcp/4001/p2p/<PEER_ID>")
31+
32+
# Connected peers from p2p_status.json
33+
click.echo()
34+
status_path = config.node.data_dir / "p2p_status.json"
35+
if status_path.exists():
36+
try:
37+
data = json.loads(status_path.read_text())
38+
state = data.get("state", "stopped")
39+
peers = data.get("peer_ids", [])
40+
bs = data.get("bootstrap", {})
41+
click.echo(f"P2P state: {state}")
42+
click.echo(f"Connected peers: {len(peers)}")
43+
for pid in peers:
44+
click.echo(f" {pid}")
45+
46+
if isinstance(bs, dict) and bs:
47+
bs_conn = bs.get("connected", 0)
48+
bs_fail = bs.get("failed", 0)
49+
click.echo(f"Bootstrap: {bs_conn} connected, {bs_fail} failed")
50+
failed = bs.get("failed_addrs", [])
51+
if isinstance(failed, list):
52+
for fa in failed:
53+
click.echo(" " + click.style(f"✗ {fa}", fg="red"))
54+
except (json.JSONDecodeError, OSError):
55+
click.echo("P2P state: unknown (status file unreadable)")
56+
else:
57+
click.echo("P2P state: not started")
58+
59+
60+
@peer_group.command()
61+
@click.argument("multiaddr")
62+
def add(multiaddr: str) -> None:
63+
"""Add a bootstrap node to config.
64+
65+
MULTIADDR: libp2p multiaddr, e.g.
66+
/ip4/1.2.3.4/tcp/4001/p2p/12D3KooW...
67+
"""
68+
# Validate format
69+
if not multiaddr.startswith("/ip4/") and not multiaddr.startswith("/ip6/"):
70+
click.echo(click.style("Error: ", fg="red") + "Invalid multiaddr format.")
71+
click.echo("Expected: /ip4/<IP>/tcp/<PORT>/p2p/<PEER_ID>")
72+
return
73+
74+
if "/p2p/" not in multiaddr:
75+
click.echo(
76+
click.style("Warning: ", fg="yellow") + "No /p2p/<PEER_ID> in address."
77+
" Connection may fail without peer ID."
78+
)
79+
80+
config = load_config()
81+
current = list(config.network.bootstrap_nodes)
82+
83+
if multiaddr in current:
84+
click.echo("Already in bootstrap list.")
85+
return
86+
87+
current.append(multiaddr)
88+
89+
# Update config
90+
from dataclasses import replace as dc_replace
91+
92+
from infomesh.config import save_config
93+
94+
new_net = dc_replace(config.network, bootstrap_nodes=current)
95+
new_config = dc_replace(config, network=new_net)
96+
save_config(new_config)
97+
98+
click.echo(click.style("Added: ", fg="green") + multiaddr)
99+
click.echo(
100+
"Restart the node for changes to take effect: infomesh stop && infomesh start"
101+
)
102+
103+
# Quick connectivity test
104+
import socket
105+
106+
parts = multiaddr.split("/")
107+
try:
108+
ip_idx = parts.index("ip4") + 1
109+
tcp_idx = parts.index("tcp") + 1
110+
ip = parts[ip_idx]
111+
port = int(parts[tcp_idx])
112+
113+
click.echo(f"Testing TCP {ip}:{port}... ", nl=False)
114+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
115+
sock.settimeout(5)
116+
result = sock.connect_ex((ip, port))
117+
sock.close()
118+
if result == 0:
119+
click.echo(click.style("reachable ✓", fg="green"))
120+
else:
121+
click.echo(click.style("unreachable ✗", fg="red"))
122+
click.echo(
123+
" Ensure the bootstrap node is running"
124+
" and port is open (firewall/NSG)."
125+
)
126+
except (ValueError, IndexError):
127+
click.echo("(skipped — could not parse IP/port)")
128+
129+
130+
@peer_group.command()
131+
@click.argument("multiaddr")
132+
def remove(multiaddr: str) -> None:
133+
"""Remove a bootstrap node from config.
134+
135+
MULTIADDR: The exact multiaddr string to remove.
136+
"""
137+
config = load_config()
138+
current = list(config.network.bootstrap_nodes)
139+
140+
if multiaddr not in current:
141+
click.echo("Not found in bootstrap list.")
142+
click.echo("Current nodes:")
143+
for addr in current:
144+
click.echo(f" {addr}")
145+
return
146+
147+
current.remove(multiaddr)
148+
149+
from dataclasses import replace as dc_replace
150+
151+
from infomesh.config import save_config
152+
153+
new_net = dc_replace(config.network, bootstrap_nodes=current)
154+
new_config = dc_replace(config, network=new_net)
155+
save_config(new_config)
156+
157+
click.echo(click.style("Removed: ", fg="green") + multiaddr)
158+
159+
160+
@peer_group.command()
161+
def test() -> None:
162+
"""Test connectivity to all bootstrap nodes."""
163+
import socket
164+
165+
config = load_config()
166+
bs_nodes = config.network.bootstrap_nodes
167+
168+
if not bs_nodes:
169+
click.echo("No bootstrap nodes configured.")
170+
click.echo("Add with: infomesh peer add /ip4/<IP>/tcp/4001/p2p/<PEER_ID>")
171+
return
172+
173+
click.echo(f"Testing {len(bs_nodes)} bootstrap node(s)...\n")
174+
175+
ok = 0
176+
for addr in bs_nodes:
177+
parts = addr.split("/")
178+
try:
179+
ip_idx = parts.index("ip4") + 1
180+
tcp_idx = parts.index("tcp") + 1
181+
ip = parts[ip_idx]
182+
port = int(parts[tcp_idx])
183+
184+
click.echo(f" {addr}")
185+
click.echo(f" TCP {ip}:{port} ... ", nl=False)
186+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
187+
sock.settimeout(5)
188+
result = sock.connect_ex((ip, port))
189+
sock.close()
190+
if result == 0:
191+
click.echo(click.style("OK ✓", fg="green"))
192+
ok += 1
193+
else:
194+
click.echo(click.style("FAIL ✗", fg="red"))
195+
click.echo(" → Node not running or port blocked by firewall/NSG")
196+
except (ValueError, IndexError):
197+
click.echo(
198+
click.style(" SKIP", fg="yellow") + " — could not parse address"
199+
)
200+
201+
click.echo(f"\nResult: {ok}/{len(bs_nodes)} reachable")
202+
if ok == 0:
203+
click.echo("\nNo bootstrap nodes reachable. Peers cannot be discovered.")
204+
click.echo("Troubleshooting:")
205+
click.echo(" 1. Is the bootstrap node running?")
206+
click.echo(" 2. Is TCP port 4001 open in firewall / Azure NSG / AWS SG?")
207+
click.echo(" 3. Is the IP address correct?")
208+
click.echo(" 4. Try: nc -zv <IP> 4001 (or Test-NetConnection on Windows)")

infomesh/cli/serve.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,8 @@ def _try_start_p2p(
402402
except ImportError:
403403
log.warning(
404404
"p2p_unavailable",
405-
reason="libp2p or trio not installed",
406-
hint="pip install 'libp2p[trio]' OR uv add libp2p trio",
405+
reason="libp2p not installed",
406+
hint="pip install 'infomesh[p2p]' OR uv sync --extra p2p",
407407
)
408408
return None
409409

@@ -483,16 +483,53 @@ def status() -> None:
483483
addrs = p2p.get("listen_addrs", [])
484484
if addrs and isinstance(addrs, list):
485485
click.echo(f"P2P addrs: {', '.join(str(a) for a in addrs)}")
486+
# Show bootstrap status if peers=0
487+
bs = p2p.get("bootstrap", {})
488+
if isinstance(bs, dict) and p2p_peers == 0:
489+
bs_conf = bs.get("configured", 0)
490+
bs_conn = bs.get("connected", 0)
491+
bs_fail = bs.get("failed", 0)
492+
if bs_conf == 0:
493+
click.echo(
494+
"Bootstrap: "
495+
+ click.style("none configured", fg="yellow")
496+
)
497+
click.echo(
498+
" Add bootstrap nodes in"
499+
" ~/.infomesh/config.toml:"
500+
)
501+
click.echo(" [network]")
502+
click.echo(
503+
" bootstrap_nodes"
504+
' = ["/ip4/<IP>/tcp/4001'
505+
'/p2p/<PEER_ID>"]'
506+
)
507+
elif bs_fail > 0 and bs_conn == 0:
508+
click.echo(
509+
"Bootstrap: "
510+
+ click.style(
511+
f"all {bs_fail} nodes unreachable",
512+
fg="red",
513+
)
514+
)
515+
failed = bs.get("failed_addrs", [])
516+
if isinstance(failed, list):
517+
for fa in failed:
518+
click.echo(f" ✗ {fa}")
519+
click.echo(
520+
" Check: (1) node running?"
521+
" (2) port 4001 open?"
522+
" (3) correct IP?"
523+
)
524+
click.echo(" Test: nc -zv <IP> 4001")
486525
elif p2p_state == "error":
487526
click.echo("P2P: " + click.style("error", fg="red"))
488527
err = p2p.get("error", "")
489528
if err:
490529
click.echo(f"P2P error: {err}")
491530
elif running:
492531
click.echo("P2P: " + click.style("not connected", fg="yellow"))
493-
click.echo(
494-
" (libp2p/trio not installed or no bootstrap nodes)"
495-
)
532+
click.echo(" (libp2p not installed or no bootstrap nodes)")
496533
else:
497534
click.echo("P2P: stopped")
498535

0 commit comments

Comments
 (0)