cli_common.py
1 """ 2 Shared CLI argument parsing utilities for Ag3ntum. 3 4 Provides common argument definitions and parser building blocks for both 5 CLI (agent_cli.py) and HTTP client (agent_http.py) entry points. 6 7 Usage: 8 from .cli_common import add_common_arguments, add_task_arguments 9 10 parser = argparse.ArgumentParser() 11 add_task_arguments(parser) 12 add_common_arguments(parser) 13 """ 14 import argparse 15 from typing import Any 16 17 from .constants import DEFAULT_POLL_INTERVAL 18 19 20 # ============================================================================= 21 # Argument Group Builders 22 # ============================================================================= 23 24 def add_task_arguments(parser: argparse.ArgumentParser) -> None: 25 """ 26 Add task specification arguments to parser. 27 28 These are mutually exclusive: --task or --task-file. 29 30 Args: 31 parser: ArgumentParser to add arguments to. 32 """ 33 task_group = parser.add_mutually_exclusive_group() 34 task_group.add_argument( 35 "--task", "-t", 36 type=str, 37 help="Task description to execute" 38 ) 39 task_group.add_argument( 40 "--task-file", "-f", 41 type=str, 42 help="Path to file containing task description" 43 ) 44 45 46 def add_directory_arguments(parser: argparse.ArgumentParser) -> None: 47 """ 48 Add working directory arguments to parser. 49 50 Args: 51 parser: ArgumentParser to add arguments to. 52 """ 53 parser.add_argument( 54 "--dir", "-d", 55 type=str, 56 help="Working directory for the agent" 57 ) 58 parser.add_argument( 59 "--add-dir", "-a", 60 action="append", 61 default=[], 62 help="Additional directories the agent can access (can be repeated)" 63 ) 64 65 66 def add_session_arguments(parser: argparse.ArgumentParser) -> None: 67 """ 68 Add session management arguments to parser. 69 70 Args: 71 parser: ArgumentParser to add arguments to. 72 """ 73 parser.add_argument( 74 "--resume", "-r", 75 type=str, 76 metavar="SESSION_ID", 77 help="Resume a previous session by ID" 78 ) 79 parser.add_argument( 80 "--fork-session", 81 action="store_true", 82 help="Fork to new session when resuming instead of continuing original" 83 ) 84 parser.add_argument( 85 "--list-sessions", "-l", 86 action="store_true", 87 help="List all sessions and exit" 88 ) 89 90 91 def add_config_override_arguments(parser: argparse.ArgumentParser) -> None: 92 """ 93 Add agent configuration override arguments to parser. 94 95 These allow overriding values from agent.yaml via CLI. 96 97 Args: 98 parser: ArgumentParser to add arguments to. 99 """ 100 parser.add_argument( 101 "--model", "-m", 102 type=str, 103 default=None, 104 help="Claude model to use (overrides agent.yaml)" 105 ) 106 parser.add_argument( 107 "--max-turns", 108 type=int, 109 default=None, 110 help="Maximum number of turns (overrides agent.yaml)" 111 ) 112 parser.add_argument( 113 "--timeout", 114 type=int, 115 default=None, 116 help="Timeout in seconds (overrides agent.yaml)" 117 ) 118 parser.add_argument( 119 "--no-skills", 120 action="store_true", 121 help="Disable custom skills (overrides agent.yaml)" 122 ) 123 124 125 def add_permission_arguments(parser: argparse.ArgumentParser) -> None: 126 """ 127 Add permission configuration arguments to parser. 128 129 Args: 130 parser: ArgumentParser to add arguments to. 131 """ 132 parser.add_argument( 133 "--profile", 134 type=str, 135 metavar="PATH", 136 help="Path to permission profile file (YAML or JSON)" 137 ) 138 parser.add_argument( 139 "--permission-mode", 140 type=str, 141 choices=["default", "acceptEdits", "plan", "bypassPermissions"], 142 default=None, 143 help="Permission mode to use (overrides config file)" 144 ) 145 146 147 def add_output_arguments(parser: argparse.ArgumentParser) -> None: 148 """ 149 Add output-related arguments to parser. 150 151 Args: 152 parser: ArgumentParser to add arguments to. 153 """ 154 parser.add_argument( 155 "--output", "-o", 156 type=str, 157 help="Output file for results (default: stdout)" 158 ) 159 parser.add_argument( 160 "--json", 161 action="store_true", 162 help="Output results as JSON" 163 ) 164 165 166 def add_logging_arguments(parser: argparse.ArgumentParser) -> None: 167 """ 168 Add logging configuration arguments to parser. 169 170 Args: 171 parser: ArgumentParser to add arguments to. 172 """ 173 parser.add_argument( 174 "--log-level", 175 type=str, 176 default="INFO", 177 choices=["DEBUG", "INFO", "WARNING", "ERROR"], 178 help="Logging level (default: INFO)" 179 ) 180 181 182 def add_role_argument(parser: argparse.ArgumentParser) -> None: 183 """ 184 Add role selection argument to parser. 185 186 Args: 187 parser: ArgumentParser to add arguments to. 188 """ 189 parser.add_argument( 190 "--role", 191 type=str, 192 default=None, 193 help="Role template name (loads prompts/roles/<role>.md)" 194 ) 195 196 197 # ============================================================================= 198 # HTTP Client Specific Arguments 199 # ============================================================================= 200 201 def add_http_arguments(parser: argparse.ArgumentParser) -> None: 202 """ 203 Add HTTP client-specific arguments to parser. 204 205 Args: 206 parser: ArgumentParser to add arguments to. 207 """ 208 parser.add_argument( 209 "--poll-interval", 210 type=float, 211 default=DEFAULT_POLL_INTERVAL, 212 help=f"Polling interval in seconds (default: {DEFAULT_POLL_INTERVAL})" 213 ) 214 parser.add_argument( 215 "--no-wait", 216 action="store_true", 217 help="Don't wait for task completion, just start and exit" 218 ) 219 parser.add_argument( 220 "--api-url", 221 type=str, 222 default=None, 223 help="Override API base URL (default: from config/api.yaml)" 224 ) 225 226 227 # ============================================================================= 228 # CLI Specific Arguments 229 # ============================================================================= 230 231 def add_cli_arguments(parser: argparse.ArgumentParser) -> None: 232 """ 233 Add CLI-specific arguments to parser. 234 235 Args: 236 parser: ArgumentParser to add arguments to. 237 """ 238 # Config file path arguments 239 parser.add_argument( 240 "--config", "-c", 241 type=str, 242 metavar="PATH", 243 help="Path to agent.yaml configuration file (default: config/agent.yaml)" 244 ) 245 parser.add_argument( 246 "--secrets", 247 type=str, 248 metavar="PATH", 249 help="Path to secrets.yaml file (default: config/secrets.yaml)" 250 ) 251 252 # Universal configuration overrides 253 parser.add_argument( 254 "--set", 255 action="append", 256 metavar="KEY=VALUE", 257 default=[], 258 help=""" 259 Override any agent.yaml value using dot notation. 260 Can be repeated. Examples: 261 --set agent.model=claude-sonnet-4-5-20250929 262 --set agent.max_turns=50 263 --set agent.permission_mode=acceptEdits""" 264 ) 265 266 # Legacy permissions config 267 parser.add_argument( 268 "--permissions-config", "-p", 269 type=str, 270 metavar="PATH", 271 help="Path to legacy permissions.json configuration file" 272 ) 273 274 # Special commands 275 parser.add_argument( 276 "--show-tools", 277 action="store_true", 278 help="Show all available tools and their descriptions" 279 ) 280 parser.add_argument( 281 "--show-permissions", 282 action="store_true", 283 help="Show current permission configuration" 284 ) 285 parser.add_argument( 286 "--init-permissions", 287 action="store_true", 288 help="Create default permissions.json in AGENT/config/" 289 ) 290 parser.add_argument( 291 "--check-profile", 292 action="store_true", 293 help=""" 294 Validate that permission profile file exists in AGENT/config/: 295 - permissions.yaml 296 Supports .yaml, .yml, and .json formats.""" 297 ) 298 parser.add_argument( 299 "--init", 300 action="store_true", 301 help="Initialize .claude settings in working directory" 302 ) 303 304 305 # ============================================================================= 306 # Parser Builders 307 # ============================================================================= 308 309 def create_common_parser( 310 description: str, 311 epilog: str = "", 312 ) -> argparse.ArgumentParser: 313 """ 314 Create a parser with common formatting settings. 315 316 Args: 317 description: Parser description. 318 epilog: Parser epilog (examples). 319 320 Returns: 321 Configured ArgumentParser. 322 """ 323 return argparse.ArgumentParser( 324 description=description, 325 formatter_class=argparse.RawDescriptionHelpFormatter, 326 epilog=epilog, 327 ) 328 329 330 # ============================================================================= 331 # Override Parsing Utilities 332 # ============================================================================= 333 334 def parse_set_overrides(set_args: list[str]) -> dict[str, Any]: 335 """ 336 Parse --set KEY=VALUE arguments into a dictionary of overrides. 337 338 Args: 339 set_args: List of "key=value" strings from --set arguments. 340 341 Returns: 342 Dictionary mapping keys to values. Nested keys use dot notation 343 (e.g., "agent.model" becomes {"model": value}). 344 345 Raises: 346 ValueError: If a --set argument is malformed. 347 """ 348 overrides: dict[str, Any] = {} 349 350 for arg in set_args: 351 if "=" not in arg: 352 raise ValueError( 353 f"Invalid --set argument: '{arg}'\n" 354 f"Expected format: KEY=VALUE (e.g., agent.model=claude-sonnet-4-5)" 355 ) 356 357 key, value = arg.split("=", 1) 358 key = key.strip() 359 value_str = value.strip() 360 361 # Remove "agent." prefix if present (for consistency) 362 if key.startswith("agent."): 363 key = key[6:] 364 365 # Type conversion for known fields 366 typed_value: Any = value_str 367 368 if key in ("max_turns", "timeout_seconds", "max_buffer_size"): 369 try: 370 typed_value = int(value_str) 371 except ValueError: 372 raise ValueError( 373 f"Invalid value for {key}: '{value_str}' (expected integer)" 374 ) 375 elif key in ("enable_skills", "enable_file_checkpointing", "include_partial_messages"): 376 typed_value = value_str.lower() in ("true", "1", "yes", "on") 377 elif value_str.lower() in ("null", "none"): 378 typed_value = None 379 380 overrides[key] = typed_value 381 382 return overrides