/ src / core / cli_common.py
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