personal memory agent
at main 251 lines 7.5 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""CLI for managing maintenance tasks. 5 6Usage: 7 sol maint # Run pending tasks 8 sol maint --list # Show status of all tasks 9 sol maint <task> # Show task details and log output 10 sol maint --force <task> # Re-run a specific task 11""" 12 13from __future__ import annotations 14 15import argparse 16import json 17import sys 18from datetime import datetime 19from pathlib import Path 20 21from think.utils import get_journal, setup_cli 22 23from .maint import ( 24 get_state_file, 25 get_task_by_name, 26 get_task_status, 27 list_tasks, 28 run_pending_tasks, 29 run_task, 30) 31 32 33def _format_duration(ms: int) -> str: 34 """Format duration in milliseconds to a compact human-readable string.""" 35 if ms < 1000: 36 return f"{ms}ms" 37 if ms < 60000: 38 return f"{ms // 1000}s" 39 return f"{ms // 60000}m {(ms % 60000) // 1000}s" 40 41 42def print_task(t: dict) -> None: 43 """Print a task summary line and optional run metadata.""" 44 desc = f" - {t['description']}" if t["description"] else "" 45 status_info = "" 46 if t["status"] == "in_progress": 47 status_info = " (in progress)" 48 elif t["exit_code"] is not None and t["exit_code"] != 0: 49 status_info = f" (exit {t['exit_code']})" 50 51 print(f" {t['qualified_name']}{desc}{status_info}") 52 53 if t.get("ran_ts") is None: 54 return 55 56 ts_str = datetime.fromtimestamp(t["ran_ts"] / 1000).strftime("%Y-%m-%d %H:%M") 57 parts = [f"ran {ts_str}"] 58 detail_parts = [] 59 if t.get("duration_ms") is not None: 60 detail_parts.append(_format_duration(t["duration_ms"])) 61 line_count = t.get("line_count", 0) 62 if line_count > 0: 63 detail_parts.append(f"{line_count} lines") 64 if detail_parts: 65 parts.append(f"({', '.join(detail_parts)})") 66 67 print(f" {' '.join(parts)}") 68 69 70def show_task_details(journal: Path, task_name: str) -> None: 71 """Show details and log output for a maintenance task.""" 72 task = get_task_by_name(task_name) 73 if not task: 74 print(f"Task not found: {task_name}", file=sys.stderr) 75 print("Use 'sol maint --list' to see available tasks.", file=sys.stderr) 76 sys.exit(1) 77 78 status, exit_code, ran_ts = get_task_status(journal, task.app, task.name) 79 state_file = get_state_file(journal, task.app, task.name) 80 81 duration_ms = None 82 log_lines: list[str] = [] 83 errors: list[str] = [] 84 if status != "pending" and state_file.exists(): 85 with open(state_file, "r") as f: 86 for raw_line in f: 87 line = raw_line.strip() 88 if not line: 89 continue 90 try: 91 event = json.loads(line) 92 except json.JSONDecodeError: 93 continue 94 95 event_type = event.get("event") 96 if event_type == "line": 97 text = event.get("line") 98 if isinstance(text, str): 99 log_lines.append(text) 100 elif event_type == "exit": 101 if isinstance(event.get("duration_ms"), int): 102 duration_ms = event["duration_ms"] 103 if event.get("error"): 104 errors.append(str(event["error"])) 105 106 print(task.qualified_name) 107 if task.description: 108 print(task.description) 109 110 if status == "pending": 111 print("Status: pending") 112 elif status == "in_progress": 113 print("Status: in progress") 114 elif status == "success": 115 print("Status: success (exit 0)") 116 elif exit_code is None: 117 print("Status: failed") 118 else: 119 print(f"Status: failed (exit {exit_code})") 120 121 if ran_ts is not None: 122 ts_str = datetime.fromtimestamp(ran_ts / 1000).strftime("%Y-%m-%d %H:%M") 123 if duration_ms is not None: 124 print(f"Ran: {ts_str} ({duration_ms}ms)") 125 else: 126 print(f"Ran: {ts_str}") 127 128 if state_file.exists(): 129 print(f"Log: {state_file}") 130 131 print() 132 133 if status == "pending": 134 print("Task has not been run yet.") 135 return 136 137 for line in log_lines: 138 print(line) 139 140 for error in errors: 141 print(f"Error: {error}") 142 143 144def main() -> None: 145 """CLI entry point for sol maint command.""" 146 parser = argparse.ArgumentParser( 147 description="Run maintenance tasks for apps", 148 formatter_class=argparse.RawDescriptionHelpFormatter, 149 epilog=""" 150Examples: 151 sol maint Run all pending maintenance tasks 152 sol maint --list Show status of all tasks 153 sol maint chat:fix_x Show task details and log output 154 sol maint -f fix_x Re-run a specific task 155""", 156 ) 157 parser.add_argument( 158 "task", 159 nargs="?", 160 help="Task to show details for (or to re-run with --force)", 161 ) 162 parser.add_argument( 163 "--list", 164 "-l", 165 action="store_true", 166 help="List all tasks with their status", 167 ) 168 parser.add_argument( 169 "--force", 170 "-f", 171 action="store_true", 172 help="Re-run a specific task (requires task name)", 173 ) 174 175 args = setup_cli(parser) 176 journal = Path(get_journal()) 177 178 # List mode 179 if args.list: 180 tasks = list_tasks(journal) 181 if not tasks: 182 print("No maintenance tasks found.") 183 return 184 185 # Group by status 186 pending = [t for t in tasks if t["status"] == "pending"] 187 in_progress = [t for t in tasks if t["status"] == "in_progress"] 188 success = [t for t in tasks if t["status"] == "success"] 189 failed = [t for t in tasks if t["status"] == "failed"] 190 191 if pending: 192 print(f"Pending ({len(pending)}):") 193 for t in pending: 194 print_task(t) 195 196 if in_progress: 197 print(f"In Progress ({len(in_progress)}):") 198 for t in in_progress: 199 print_task(t) 200 201 if failed: 202 print(f"Failed ({len(failed)}):") 203 for t in failed: 204 print_task(t) 205 206 if success: 207 print(f"Completed ({len(success)}):") 208 for t in success: 209 print_task(t) 210 211 return 212 213 # Force re-run a specific task 214 if args.force: 215 if not args.task: 216 print("--force requires a task name.", file=sys.stderr) 217 print("Usage: sol maint --force <task>", file=sys.stderr) 218 sys.exit(1) 219 task = get_task_by_name(args.task) 220 if not task: 221 print(f"Task not found: {args.task}", file=sys.stderr) 222 print("Use 'sol maint --list' to see available tasks.", file=sys.stderr) 223 sys.exit(1) 224 success, exit_code = run_task(journal, task) 225 sys.exit(0 if success else exit_code) 226 227 # Show task details 228 if args.task: 229 show_task_details(journal, args.task) 230 return 231 232 # Bare invocation - show in-progress, then run pending 233 tasks = list_tasks(journal) 234 in_progress = [t for t in tasks if t["status"] == "in_progress"] 235 if in_progress: 236 print(f"In Progress ({len(in_progress)}):") 237 for t in in_progress: 238 print_task(t) 239 print() 240 241 # Run pending tasks 242 ran, succeeded = run_pending_tasks(journal) 243 if ran == 0: 244 print("No pending maintenance tasks.") 245 else: 246 print(f"Completed {succeeded}/{ran} task(s)") 247 sys.exit(0 if succeeded == ran else 1) 248 249 250if __name__ == "__main__": 251 main()