"""Run router + host client in a single process (localhost mode). Equivalent to the pre-M3 single-machine setup. Users run `python standalone.py` and get the exact same experience as `python main.py`, but the code paths use the multi-host architecture internally. """ from __future__ import annotations import asyncio import logging import secrets import sys import uvicorn logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) async def run_standalone() -> None: """Run router + host client in a single process.""" secret = secrets.token_hex(16) router_url = "ws://127.0.0.1:8000/ws/node" from router.main import create_app from host_client.main import NodeClient from host_client.config import HostConfig config = HostConfig.from_keyring() config.router_url = router_url config.router_secret = secret app = create_app(router_secret=secret) config_obj = uvicorn.Config( app, host="0.0.0.0", port=8000, log_level="info", ws_ping_interval=20, ws_ping_timeout=60, ) server = uvicorn.Server(config_obj) async def run_server(): await server.serve() async def run_client(): await asyncio.sleep(1.5) client = NodeClient(config) try: await client.run() except Exception as e: logger.exception("Host client error: %s", e) server.should_exit = True await asyncio.gather(run_server(), run_client()) def main() -> None: """Entry point.""" logger.info("Starting PhoneWork in standalone mode...") try: asyncio.run(run_standalone()) except KeyboardInterrupt: logger.info("Shutting down...") if __name__ == "__main__": main()