@@ -279,6 +279,7 @@ async def _crawl_batch() -> int:
279279 asyncio .run (_crawl_batch ())
280280 elapsed = time .monotonic () - start_time
281281 click .echo (f"\n Initial crawl: { crawled } pages indexed in { elapsed :.1f} s" )
282+ ctx .close ()
282283
283284
284285# ---------------------------------------------------------------------------
@@ -314,9 +315,9 @@ def stop() -> None:
314315
315316def _kill_bgm_processes () -> None :
316317 """Find and kill BGM player processes spawned by infomesh."""
317- from infomesh .dashboard .bgm import _kill_orphaned_bgm
318+ from infomesh .dashboard .bgm import kill_orphaned_bgm
318319
319- _kill_orphaned_bgm ()
320+ kill_orphaned_bgm ()
320321
321322
322323# ---------------------------------------------------------------------------
@@ -391,16 +392,24 @@ def serve(seeds: str | None, role: str | None) -> None:
391392 _serve_logger .debug ("credit_sync_init_skipped" )
392393
393394 # ββ P2P node (best-effort) βββββββββββββββββββββββββββββββββ
394- p2p_node = _try_start_p2p (
395+ # Build local_search_fn so peers can query our local index.
396+ from infomesh .services import bootstrap_p2p , create_local_search_fn
397+
398+ _local_search_fn = create_local_search_fn (config )
399+
400+ p2p_node , _distributed_index = bootstrap_p2p (
395401 config ,
396- _serve_logger ,
397402 credit_sync_manager = _credit_sync_mgr ,
403+ local_search_fn = _local_search_fn ,
398404 )
399405
400406 async def _run () -> None :
401407 from infomesh .services import AppContext , seed_and_crawl_loop
402408
403409 ctx = AppContext (config )
410+ # Attach P2P components so MCP and search can use them
411+ ctx .distributed_index = _distributed_index
412+ ctx .p2p_node = p2p_node
404413
405414 if config .node .role == NodeRole .SEARCH :
406415 # Search-only nodes don't crawl β wait for index submissions
@@ -427,63 +436,105 @@ async def _run() -> None:
427436 _serve_logger .info ("serve_stopped" )
428437
429438
430- def _try_start_p2p (
439+ # ---------------------------------------------------------------------------
440+ # infomesh status
441+ # ---------------------------------------------------------------------------
442+
443+
444+ def _render_p2p_status (
431445 config : Config ,
432- log : structlog .stdlib .BoundLogger ,
433- * ,
434- credit_sync_manager : object | None = None ,
435- ) -> object | None :
436- """Try to start the P2P node. Returns the node or None on failure.
446+ running : bool ,
447+ ) -> None :
448+ """Render P2P status lines for the ``status`` command."""
449+ from infomesh .dashboard .utils import read_p2p_status
450+
451+ p2p = read_p2p_status (config )
452+ p2p_state = p2p .get ("state" , "stopped" )
453+ p2p_peers = p2p .get ("peers" , 0 )
454+
455+ if p2p_state == "running" :
456+ click .echo (f"P2P: running ({ p2p_peers } peers)" )
457+ addrs = p2p .get ("listen_addrs" , [])
458+ if addrs and isinstance (addrs , list ):
459+ click .echo ("P2P addrs: " + ", " .join (str (a ) for a in addrs ))
460+ bs = p2p .get ("bootstrap" , {})
461+ if isinstance (bs , dict ) and p2p_peers == 0 :
462+ _render_bootstrap_hints (bs )
463+ elif p2p_state == "error" :
464+ click .echo ("P2P: " + click .style ("error" , fg = "red" ))
465+ err = p2p .get ("error" , "" )
466+ if err :
467+ click .echo (f"P2P error: { err } " )
468+ elif running :
469+ click .echo ("P2P: " + click .style ("not connected" , fg = "yellow" ))
470+ click .echo (" (libp2p not installed or no bootstrap nodes)" )
471+ else :
472+ click .echo ("P2P: stopped" )
437473
438- Failures are logged as warnings β the node continues in local-only mode.
439- """
440- try :
441- from infomesh .p2p .node import InfoMeshNode # noqa: F811
442- except ImportError :
443- log .warning (
444- "p2p_unavailable" ,
445- reason = "libp2p not installed" ,
446- hint = "pip install 'infomesh[p2p]'" ,
447- )
448- return None
449474
450- try :
451- node = InfoMeshNode (
452- config ,
453- credit_sync_manager = credit_sync_manager ,
454- )
455- node .start (blocking = False )
456- log .info (
457- "p2p_started" ,
458- peer_id = node .peer_id ,
459- listen_port = config .node .listen_port ,
460- bootstrap_nodes = len (config .network .bootstrap_nodes ),
475+ def _render_bootstrap_hints (
476+ bs : dict [str , object ],
477+ ) -> None :
478+ """Render bootstrap troubleshooting hints."""
479+ bs_conf = bs .get ("configured" , 0 )
480+ bs_conn = bs .get ("connected" , 0 )
481+ bs_fail = bs .get ("failed" , 0 )
482+
483+ if bs_conf == 0 :
484+ click .echo ("Bootstrap: " + click .style ("none configured" , fg = "yellow" ))
485+ click .echo (" Add bootstrap nodes in ~/.infomesh/config.toml:" )
486+ click .echo (" [network]" )
487+ click .echo (
488+ ' bootstrap_nodes = ["/ip4/<IP>/tcp/4001/p2p/<PEER_ID>"]'
461489 )
462- if not config .network .bootstrap_nodes :
463- log .warning (
464- "p2p_no_bootstrap" ,
465- msg = (
466- "No bootstrap nodes configured. "
467- "Add [network] bootstrap_nodes in "
468- "~/.infomesh/config.toml to connect to peers."
469- ),
490+ elif isinstance (bs_fail , int ) and bs_fail > 0 and bs_conn == 0 :
491+ click .echo (
492+ "Bootstrap: "
493+ + click .style (
494+ f"all { bs_fail } nodes unreachable" ,
495+ fg = "red" ,
470496 )
471- return node
472- except Exception as exc :
473- log .warning (
474- "p2p_start_failed" ,
475- error = str (exc ),
476- msg = (
477- "P2P node failed to start β running in local-only mode. "
478- "Crawling, indexing, and local search still work."
479- ),
480497 )
481- return None
498+ failed = bs .get ("failed_addrs" , [])
499+ if isinstance (failed , list ):
500+ for fa in failed :
501+ click .echo (f" β { fa } " )
502+ click .echo (
503+ " Check: (1) node running?"
504+ " (2) port 4001 open?"
505+ " (3) correct IP?"
506+ )
507+ click .echo (" Test: nc -zv <IP> 4001" )
508+
509+
510+ def _render_credit_status (ledger : object ) -> None :
511+ """Render credit status lines for the ``status`` command."""
512+ if ledger is None :
513+ click .echo ("Credits: N/A (ledger unavailable)" )
514+ return
515+ ls = ledger .stats () # type: ignore[attr-defined]
516+ click .echo (
517+ f"Credits: { ls .balance :.1f} "
518+ f" (earned { ls .total_earned :.1f} "
519+ f" / spent { ls .total_spent :.1f} )"
520+ )
521+ click .echo (
522+ f"Tier: { ls .tier .value } "
523+ f" (score { ls .contribution_score :.1f} ,"
524+ f" search cost { ls .search_cost :.3f} )"
525+ )
526+ if ls .credit_state .value != "normal" :
527+ click .echo (f"Credit state: { ls .credit_state .value } " )
528+ if ls .owner_email :
529+ click .echo (f"GitHub: { ls .owner_email } " )
530+ click .echo (" Credits linked across all nodes." )
531+ else :
532+ click .echo ("GitHub: " + click .style ("not connected" , fg = "yellow" ))
533+ click .echo (
534+ " Run 'infomesh config github [email protected] ' to link." 535+ )
482536
483537
484- # ---------------------------------------------------------------------------
485- # infomesh status
486- # ---------------------------------------------------------------------------
487538@click .command ()
488539def status () -> None :
489540 """Show node status."""
@@ -515,95 +566,8 @@ def status() -> None:
515566 click .echo ("Vector docs: (chromadb not installed)" )
516567 click .echo (f"LLM: { 'on' if config .llm .enabled else 'off' } " )
517568
518- # ββ P2P status βββββββββββββββββββββββββββββββββββββββββ
519- from infomesh .dashboard .utils import read_p2p_status
520-
521- p2p = read_p2p_status (config )
522- p2p_state = p2p .get ("state" , "stopped" )
523- p2p_peers = p2p .get ("peers" , 0 )
524- if p2p_state == "running" :
525- click .echo (f"P2P: running ({ p2p_peers } peers)" )
526- addrs = p2p .get ("listen_addrs" , [])
527- if addrs and isinstance (addrs , list ):
528- click .echo (f"P2P addrs: { ', ' .join (str (a ) for a in addrs )} " )
529- # Show bootstrap status if peers=0
530- bs = p2p .get ("bootstrap" , {})
531- if isinstance (bs , dict ) and p2p_peers == 0 :
532- bs_conf = bs .get ("configured" , 0 )
533- bs_conn = bs .get ("connected" , 0 )
534- bs_fail = bs .get ("failed" , 0 )
535- if bs_conf == 0 :
536- click .echo (
537- "Bootstrap: "
538- + click .style ("none configured" , fg = "yellow" )
539- )
540- click .echo (
541- " Add bootstrap nodes in"
542- " ~/.infomesh/config.toml:"
543- )
544- click .echo (" [network]" )
545- click .echo (
546- " bootstrap_nodes"
547- ' = ["/ip4/<IP>/tcp/4001'
548- '/p2p/<PEER_ID>"]'
549- )
550- elif bs_fail > 0 and bs_conn == 0 :
551- click .echo (
552- "Bootstrap: "
553- + click .style (
554- f"all { bs_fail } nodes unreachable" ,
555- fg = "red" ,
556- )
557- )
558- failed = bs .get ("failed_addrs" , [])
559- if isinstance (failed , list ):
560- for fa in failed :
561- click .echo (f" β { fa } " )
562- click .echo (
563- " Check: (1) node running?"
564- " (2) port 4001 open?"
565- " (3) correct IP?"
566- )
567- click .echo (" Test: nc -zv <IP> 4001" )
568- elif p2p_state == "error" :
569- click .echo ("P2P: " + click .style ("error" , fg = "red" ))
570- err = p2p .get ("error" , "" )
571- if err :
572- click .echo (f"P2P error: { err } " )
573- elif running :
574- click .echo ("P2P: " + click .style ("not connected" , fg = "yellow" ))
575- click .echo (" (libp2p not installed or no bootstrap nodes)" )
576- else :
577- click .echo ("P2P: stopped" )
578-
579- # Credits
580- if ctx .ledger is not None :
581- ls = ctx .ledger .stats ()
582- click .echo (
583- f"Credits: { ls .balance :.1f} "
584- f" (earned { ls .total_earned :.1f} "
585- f" / spent { ls .total_spent :.1f} )"
586- )
587- click .echo (
588- f"Tier: { ls .tier .value } "
589- f" (score { ls .contribution_score :.1f} ,"
590- f" search cost { ls .search_cost :.3f} )"
591- )
592- if ls .credit_state .value != "normal" :
593- click .echo (f"Credit state: { ls .credit_state .value } " )
594- if ls .owner_email :
595- click .echo (f"GitHub: { ls .owner_email } " )
596- click .echo (" Credits linked across all nodes." )
597- else :
598- click .echo (
599- "GitHub: " + click .style ("not connected" , fg = "yellow" )
600- )
601- click .echo (
602- " Run "
603- "'infomesh config github [email protected] ' to link." 604- )
605- else :
606- click .echo ("Credits: N/A (ledger unavailable)" )
569+ _render_p2p_status (config , running )
570+ _render_credit_status (ctx .ledger )
607571
608572 keys_dir = config .node .data_dir / "keys"
609573 if (keys_dir / "private.pem" ).exists ():
0 commit comments