From 70bb19e1d9f1da351e655f44ee8ebdcf4e502b91 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Fri, 10 Apr 2026 00:44:54 +0000 Subject: [PATCH] fix(cli): add [REGEX] positional argument to actor context list (#6500) ISSUES CLOSED: #6500 --- features/actor_context_cmds.feature | 9 ++++ features/steps/actor_context_cmds_steps.py | 51 +++++++++++++++++++ robot/cli_plan_context_commands.robot | 19 +++++++ .../cli/commands/actor_context.py | 43 ++++++++++++++++ src/cleveragents/cli/commands/context.py | 43 +++++++++++++--- 5 files changed, 158 insertions(+), 7 deletions(-) diff --git a/features/actor_context_cmds.feature b/features/actor_context_cmds.feature index 6412514b2..36f6e1e4b 100644 --- a/features/actor_context_cmds.feature +++ b/features/actor_context_cmds.feature @@ -6,6 +6,15 @@ Feature: Actor context remove, export, and import commands Background: Given a temporary context directory for actor context tests + @actor_context_list_regex + Scenario: List actor contexts filtered by regex + Given an actor context named "docs" exists + And an actor context named "notes" exists + When I run actor context list "docs" + Then the actor context list command should succeed + And the list output should contain "docs" + And the list output should not contain "notes" + # ── context remove ───────────────────────────────────────── Scenario: Remove a named actor context diff --git a/features/steps/actor_context_cmds_steps.py b/features/steps/actor_context_cmds_steps.py index cd831971a..470e1ed08 100644 --- a/features/steps/actor_context_cmds_steps.py +++ b/features/steps/actor_context_cmds_steps.py @@ -105,6 +105,36 @@ def step_create_json_file_with_name(context, name): context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8") +# --------------------------------------------------------------------------- +# When — list +# --------------------------------------------------------------------------- + + +@when('I run actor context list "{pattern}"') +def step_list_with_pattern(context, pattern): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + pattern, + "--context-dir", + str(context.context_dir), + ], + ) + + +@when("I run actor context list without a pattern") +def step_list_without_pattern(context): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + "--context-dir", + str(context.context_dir), + ], + ) + + # --------------------------------------------------------------------------- # When — remove # --------------------------------------------------------------------------- @@ -330,6 +360,15 @@ def step_roundtrip_import(context, name): # --------------------------------------------------------------------------- +@then("the actor context list command should succeed") +def step_list_success(context): + assert context.result.exit_code == 0, ( + f"Expected exit 0, got {context.result.exit_code}.\n" + f"stdout: {context.result.output}\n" + f"stderr: {getattr(context.result, 'stderr', '')}" + ) + + @then("the actor context remove command should succeed") def step_remove_success(context): assert context.result.exit_code == 0, ( @@ -414,6 +453,18 @@ def step_context_exists_check(context, name): # --------------------------------------------------------------------------- +@then('the list output should contain "{text}"') +def step_list_output_contains(context, text): + output = context.result.output + assert text in output, f"Expected '{text}' in list output: {output}" + + +@then('the list output should not contain "{text}"') +def step_list_output_not_contains(context, text): + output = context.result.output + assert text not in output, f"Did not expect '{text}' in list output: {output}" + + @then('the output should contain valid JSON with key "{key}"') def step_output_json_key(context, key): output = context.result.output diff --git a/robot/cli_plan_context_commands.robot b/robot/cli_plan_context_commands.robot index b8ef9084e..3f2106d0c 100644 --- a/robot/cli_plan_context_commands.robot +++ b/robot/cli_plan_context_commands.robot @@ -179,6 +179,25 @@ List Context Files [Teardown] Cleanup Test Directory +Actor Context List Supports Regex Filter + [Documentation] Verify actor context list supports optional regex filtering + [Setup] Initialize Test Project With Context + + ${result}= Run Process ${PYTHON} -m cleveragents actor context list test + ... cwd=${TEST_DIR} timeout=120s on_timeout=kill + + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} test.py + Should Not Contain ${result.stdout} utils.py + + ${result}= Run Process ${PYTHON} -m cleveragents actor context list nomatch + ... cwd=${TEST_DIR} timeout=120s on_timeout=kill + + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} No contexts found. + + [Teardown] Cleanup Test Directory + Show Context Content [Documentation] Test context show command [Setup] Initialize Test Project With Context diff --git a/src/cleveragents/cli/commands/actor_context.py b/src/cleveragents/cli/commands/actor_context.py index 948a2ff21..028948d62 100644 --- a/src/cleveragents/cli/commands/actor_context.py +++ b/src/cleveragents/cli/commands/actor_context.py @@ -11,6 +11,7 @@ from __future__ import annotations import hashlib import json +import re from pathlib import Path from typing import Annotated, Any @@ -29,6 +30,48 @@ console = Console() _FORMAT_HELP = "Output format: json, yaml, plain, table, rich, or color (default: rich)" + +@app.command("list") +def context_list( + regex: Annotated[ + str | None, + typer.Argument( + help="Optional regex filter for context names", + ), + ] = None, + context_dir: Annotated[ + Path | None, + typer.Option( + "--context-dir", + help="Directory where contexts are stored", + resolve_path=True, + ), + ] = None, +) -> None: + """List named actor contexts, optionally filtered by a regular expression.""" + + pattern: re.Pattern[str] | None = None + if regex is not None: + try: + pattern = re.compile(regex) + except re.error as exc: + console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}") + raise typer.Abort() from exc + + base = _default_context_base(context_dir) + context_names = _list_context_names(base) + + if pattern is not None: + context_names = [name for name in context_names if pattern.search(name)] + + if not context_names: + typer.echo("No contexts found.") + return + + for name in context_names: + typer.echo(name) + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/src/cleveragents/cli/commands/context.py b/src/cleveragents/cli/commands/context.py index 1e4a902bd..221ccce21 100644 --- a/src/cleveragents/cli/commands/context.py +++ b/src/cleveragents/cli/commands/context.py @@ -9,6 +9,7 @@ Deprecated alias: ``agents context `` (emits deprecation warning) from __future__ import annotations +import re from pathlib import Path from typing import TYPE_CHECKING, Annotated, Any @@ -396,6 +397,12 @@ def context_rm_deprecated( @app.command("list") def context_list( + regex: Annotated[ + str | None, + typer.Argument( + help="Optional regex filter for context names", + ), + ] = None, context_dir: Annotated[ Path | None, typer.Option( @@ -409,6 +416,14 @@ def context_list( Or list named contexts in a directory. """ + pattern: re.Pattern[str] | None = None + if regex is not None: + try: + pattern = re.compile(regex) + except re.error as exc: + console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}") + raise typer.Abort() from exc + if context_dir is not None: # List named contexts in the given directory ctx_base = context_dir @@ -417,6 +432,8 @@ def context_list( return context_dirs = sorted([d.name for d in ctx_base.iterdir() if d.is_dir()]) + if pattern is not None: + context_dirs = [name for name in context_dirs if pattern.search(name)] if not context_dirs: typer.echo("No contexts found.") @@ -451,24 +468,36 @@ def context_list( console.print("Use 'agents actor context add ' to add files.") return - # Display context files - table = Table(title=f"Context Files ({len(context_files)} total)") - table.add_column("File Path", style="cyan") - table.add_column("Type", style="green") - table.add_column("Size", style="magenta") - table.add_column("Added", style="yellow") - + # Filter and normalize entries + filtered_entries: list[tuple[str, str, str, str]] = [] for file_info in context_files: path_text, type_label, size, added_at, _ = _normalize_context_entry( file_info ) display_name = Path(path_text).name if path_text else "" + if pattern is not None and not pattern.search(display_name): + continue + if isinstance(size, str): size_str = size else: size_str = f"{size:,} bytes" if size > 0 else "0 bytes" + filtered_entries.append((display_name, type_label, size_str, added_at)) + + if not filtered_entries: + console.print("[yellow]No files matched the pattern.[/yellow]") + return + + # Display context files + table = Table(title=f"Context Files ({len(filtered_entries)} total)") + table.add_column("File Path", style="cyan") + table.add_column("Type", style="green") + table.add_column("Size", style="magenta") + table.add_column("Added", style="yellow") + + for display_name, type_label, size_str, added_at in filtered_entries: table.add_row( display_name, type_label,