personal memory agent
at main 322 lines 9.0 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Unified CLI for solstone - AI-driven desktop journaling toolkit. 5 6Usage: 7 sol Show status and available commands 8 sol <command> [args] Run a subcommand 9 sol <module> [args] Run by module path (e.g., sol think.importers.cli) 10 11Examples: 12 sol import data.json Import data into journal 13 sol dream 20250101 Run daily processing for a day 14 sol think.agents -h Show help for specific module 15""" 16 17from __future__ import annotations 18 19import importlib 20import os 21import sys 22from typing import Any 23 24import setproctitle 25 26# ============================================================================= 27# Command Registry 28# ============================================================================= 29# Maps short command names to module paths. 30# All modules must have a main() function as entry point. 31# 32# To add a new command: 33# 1. Add entry here: "name": "package.module" 34# 2. Ensure module has main() function 35# 36# Aliases for compound commands can be added to ALIASES dict below. 37# ============================================================================= 38 39COMMANDS: dict[str, str] = { 40 # think package - daily processing and analysis 41 "import": "think.importers.cli", 42 "dream": "think.dream", 43 "planner": "think.planner", 44 "indexer": "think.indexer", 45 "supervisor": "think.supervisor", 46 "schedule": "think.scheduler", 47 "detect-created": "think.detect_created", 48 "top": "think.top", 49 "health": "think.health_cli", 50 "callosum": "think.callosum", 51 "notify": "think.notify_cli", 52 "streams": "think.streams", 53 "journal-stats": "think.journal_stats", 54 "config": "think.config_cli", 55 "formatter": "think.formatters", 56 # observe package - multimodal capture 57 "transcribe": "observe.transcribe", 58 "describe": "observe.describe", 59 "sense": "observe.sense", 60 "sync": "observe.sync", 61 "transfer": "observe.transfer", 62 "remote": "observe.remote_cli", 63 # AI agents (talent package) 64 "agents": "think.agents", 65 "cortex": "think.cortex", 66 "talent": "think.talent_cli", 67 "call": "think.call", 68 "engage": "think.engage", 69 "help": "think.help_cli", 70 "chat": "think.chat_cli", 71 "heartbeat": "think.heartbeat", 72 # convey package - web UI 73 "convey": "convey.cli", 74 "restart-convey": "convey.restart", 75 "screenshot": "convey.screenshot", 76 "maint": "convey.maint_cli", 77 "service": "think.service", 78} 79 80# ============================================================================= 81# Aliases for Compound Commands 82# ============================================================================= 83# Maps alias names to (module, default_args) tuples. 84# These provide shortcuts for common operations with preset arguments. 85# 86# Example: "reindex": ("think.indexer", ["--rescan"]) 87# Running "sol reindex" is equivalent to "sol indexer --rescan" 88# ============================================================================= 89 90ALIASES: dict[str, tuple[str, list[str]]] = { 91 "start": ("think.supervisor", []), 92 "up": ("think.service", ["up"]), 93 "down": ("think.service", ["down"]), 94} 95 96# Command groupings for help display 97GROUPS: dict[str, list[str]] = { 98 "Think (daily processing)": [ 99 "import", 100 "dream", 101 "planner", 102 "indexer", 103 "supervisor", 104 "schedule", 105 "top", 106 "health", 107 "callosum", 108 "notify", 109 "heartbeat", 110 ], 111 "Service": ["service"], 112 "Observe (capture)": [ 113 "transcribe", 114 "describe", 115 "sense", 116 "sync", 117 "transfer", 118 "remote", 119 ], 120 "Talent (AI agents)": [ 121 "agents", 122 "cortex", 123 "talent", 124 "call", 125 "engage", 126 ], 127 "Convey (web UI)": [ 128 "convey", 129 "restart-convey", 130 "screenshot", 131 "maint", 132 ], 133 "Specialized tools": [ 134 "config", 135 "streams", 136 "journal-stats", 137 "formatter", 138 "detect-created", 139 ], 140 "Help": ["help", "chat"], 141} 142 143 144def get_status() -> dict[str, Any]: 145 """Return current journal status information.""" 146 from think.utils import get_journal_info 147 148 path, source = get_journal_info() 149 150 return { 151 "journal_path": path, 152 "journal_source": source, 153 "journal_exists": os.path.isdir(path), 154 } 155 156 157def print_status() -> None: 158 """Print current journal status.""" 159 status = get_status() 160 161 print(f"Journal: {status['journal_path']}") 162 if status["journal_exists"]: 163 # Count day directories 164 journal = status["journal_path"] 165 days = [ 166 d 167 for d in os.listdir(journal) 168 if os.path.isdir(os.path.join(journal, d)) and d.isdigit() and len(d) == 8 169 ] 170 print(f"Days: {len(days)}") 171 print() 172 173 174def print_help() -> None: 175 """Print help with status and available commands.""" 176 print("sol - solstone unified CLI\n") 177 print_status() 178 179 print("Usage: sol <command> [args...]\n") 180 181 # Print grouped commands 182 for group_name, commands in GROUPS.items(): 183 print(f"{group_name}:") 184 for cmd in commands: 185 if cmd in COMMANDS: 186 module = COMMANDS[cmd] 187 print(f" {cmd:16} {module}") 188 print() 189 190 # Print aliases if any 191 if ALIASES: 192 print("Aliases:") 193 for alias, (module, args) in ALIASES.items(): 194 args_str = " ".join(args) if args else "" 195 print(f" {alias:16}{module} {args_str}") 196 print() 197 198 print("Direct module syntax: sol <module.path> [args]") 199 print("Example: sol think.importers.cli --help") 200 201 202def resolve_command(name: str) -> tuple[str, list[str]]: 203 """Resolve command name to module path and any preset args. 204 205 Args: 206 name: Command name, alias, or module path 207 208 Returns: 209 Tuple of (module_path, preset_args) 210 211 Raises: 212 ValueError: If command not found 213 """ 214 # Check aliases first (they override commands) 215 if name in ALIASES: 216 module, preset_args = ALIASES[name] 217 return module, preset_args 218 219 # Check command registry 220 if name in COMMANDS: 221 return COMMANDS[name], [] 222 223 # Check if it looks like a module path (contains ".") 224 if "." in name: 225 return name, [] 226 227 # Not found 228 available = sorted(set(COMMANDS.keys()) | set(ALIASES.keys())) 229 raise ValueError( 230 f"Unknown command: {name}\nAvailable commands: {', '.join(available[:10])}..." 231 ) 232 233 234def run_command(module_path: str) -> int: 235 """Import and run a module's main() function. 236 237 Args: 238 module_path: Dotted module path (e.g., "think.importers.cli") 239 240 Returns: 241 Exit code (0 for success) 242 """ 243 try: 244 module = importlib.import_module(module_path) 245 except ImportError as e: 246 print(f"Error: Could not import module '{module_path}': {e}", file=sys.stderr) 247 return 1 248 249 if not hasattr(module, "main"): 250 print(f"Error: Module '{module_path}' has no main() function", file=sys.stderr) 251 return 1 252 253 # Call main - it may call sys.exit() internally 254 try: 255 module.main() 256 return 0 257 except SystemExit as e: 258 # Preserve exit code from subcommand 259 # SystemExit can have int code, string message, or None 260 if isinstance(e.code, int): 261 return e.code 262 elif isinstance(e.code, str): 263 print(e.code, file=sys.stderr) 264 return 1 265 else: 266 return 0 if not e.code else 1 267 268 269def main() -> None: 270 """Main entry point for sol CLI.""" 271 # No arguments - show status and help 272 if len(sys.argv) < 2: 273 print_help() 274 return 275 276 cmd = sys.argv[1] 277 278 # Help flags 279 if cmd in ("--help", "-h"): 280 print_help() 281 return 282 if cmd == "help" and len(sys.argv) <= 2: 283 print_help() 284 return 285 286 # Version flag 287 if cmd in ("--version", "-V"): 288 print("sol (solstone) 0.1.0") 289 return 290 291 # Path flag 292 if cmd == "--path": 293 from think.utils import get_journal_info 294 295 path, _source = get_journal_info() 296 print(path) 297 return 298 299 # Resolve command to module path 300 try: 301 module_path, preset_args = resolve_command(cmd) 302 except ValueError as e: 303 print(f"Error: {e}", file=sys.stderr) 304 sys.exit(1) 305 306 # Set process title for ps/top visibility 307 setproctitle.setproctitle(f"sol:{cmd}") 308 309 # Adjust sys.argv for the subcommand 310 # Original: ["sol", "import", "--day", "20250101"] 311 # Becomes: ["sol import", "--day", "20250101"] 312 # This makes argparse show "usage: sol import ..." in help 313 remaining_args = sys.argv[2:] 314 sys.argv = [f"sol {cmd}"] + preset_args + remaining_args 315 316 # Run the command 317 exit_code = run_command(module_path) 318 sys.exit(exit_code) 319 320 321if __name__ == "__main__": 322 main()