personal memory agent
at main 88 lines 2.7 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""CLI interface for app tools via Typer. 5 6Provides ``sol call <app> <command> [args]`` as a human-friendly CLI that 7parallels app tool functions. Each app can contribute a ``call.py`` 8module exporting a ``app = typer.Typer()`` instance whose commands are 9auto-discovered and mounted as sub-commands. 10 11Discovery scans ``apps/*/call.py``, imports modules, and mounts subcommands. 12""" 13 14import importlib 15import logging 16from pathlib import Path 17 18import typer 19 20logger = logging.getLogger(__name__) 21 22call_app = typer.Typer( 23 name="call", 24 help="Call app functions from the command line.", 25 no_args_is_help=True, 26) 27 28 29def _discover_app_calls() -> None: 30 """Discover and mount Typer sub-apps from apps/*/call.py. 31 32 Each ``call.py`` must export an ``app`` variable that is a 33 ``typer.Typer`` instance. The app directory name becomes the 34 sub-command name (e.g. ``sol call todos list ...``). 35 36 Errors in one app do not prevent others from loading. 37 """ 38 apps_dir = Path(__file__).parent.parent / "apps" 39 40 if not apps_dir.exists(): 41 logger.debug("No apps/ directory found, skipping app call discovery") 42 return 43 44 for app_dir in sorted(apps_dir.iterdir()): 45 if not app_dir.is_dir() or app_dir.name.startswith("_"): 46 continue 47 48 call_file = app_dir / "call.py" 49 if not call_file.exists(): 50 continue 51 52 app_name = app_dir.name 53 54 try: 55 module = importlib.import_module(f"apps.{app_name}.call") 56 57 sub_app = getattr(module, "app", None) 58 if not isinstance(sub_app, typer.Typer): 59 logger.warning( 60 f"apps/{app_name}/call.py has no 'app' Typer instance, skipping" 61 ) 62 continue 63 64 call_app.add_typer(sub_app, name=app_name) 65 logger.info(f"Loaded CLI commands from app: {app_name}") 66 except Exception as e: 67 logger.error( 68 f"Failed to load CLI from app '{app_name}': {e}", exc_info=True 69 ) 70 71 72_discover_app_calls() 73 74# Mount built-in CLIs (not auto-discovered since they live under think/) 75from think.tools.call import app as journal_app 76from think.tools.navigate import app as navigate_app 77from think.tools.routines import app as routines_app 78from think.tools.sol import app as sol_app 79 80call_app.add_typer(journal_app, name="journal") 81call_app.add_typer(navigate_app, name="navigate") 82call_app.add_typer(routines_app, name="routines") 83call_app.add_typer(sol_app, name="identity") 84 85 86def main() -> None: 87 """Entry point for ``sol call``.""" 88 call_app()