From 0b6bdfcc909bb6744d9f57fe72c716eddf125b7c Mon Sep 17 00:00:00 2001 From: CleverThis Date: Mon, 11 May 2026 02:39:12 +0000 Subject: [PATCH 1/3] fix(cli): implement missing actor context list and show commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new subcommands to `agents actor context`: - `context list []` — lists all actor contexts with optional regex filter; supports rich table, JSON, YAML, plain, color, and table output formats. - `context show ` — displays a summary of a named context including message count, total size in KB, estimated token usage, and creation timestamp. Both commands follow the same output format conventions as existing commands (remove/export/import/clear) using rich panels for default rendering. Closes #4748 --- features/actor_context_cmds.feature | 66 +++++- features/steps/actor_context_cmds_steps.py | 116 +++++++++- .../cli/commands/actor_context.py | 210 +++++++++++++++++- 3 files changed, 388 insertions(+), 4 deletions(-) diff --git a/features/actor_context_cmds.feature b/features/actor_context_cmds.feature index 2e716f9bd..e6e891aca 100644 --- a/features/actor_context_cmds.feature +++ b/features/actor_context_cmds.feature @@ -1,7 +1,7 @@ -Feature: Actor context clear, remove, export, and import commands +Feature: Actor context list, clear, show, remove, export, and import commands As a CleverAgents user I want to manage actor contexts via the CLI - So that I can remove, export, and import conversation contexts + So that I can inspect, filter, remove, export, and import conversation contexts Background: Given a temporary context directory for actor context tests @@ -135,3 +135,65 @@ Feature: Actor context clear, remove, export, and import commands And I import the context from that JSON file as "roundtrip" Then the context "roundtrip" should exist And the imported context should have the same messages as the original + + # ── context list ──────────────────────────────────────────── + + Scenario: List all actor contexts + Given an actor context named "docs" exists with messages + And an actor context named "notes" exists with messages + When I run actor context list + Then the actor context list command should succeed + + Scenario: List actor contexts with matching regex filter + Given an actor context named "docs" exists with messages + And an actor context named "docs_backup" exists with messages + And an actor context named "notes" exists with messages + When I run actor context list "docs" + Then the actor context list command should succeed + + Scenario: List empty when regex filter matches nothing + Given an actor context named "docs" exists with messages + And an actor context named "notes" exists with messages + When I run actor context list "^zzz_" + Then the actor context list command should succeed + And the output should contain valid JSON with key "context_list" + + Scenario: List actor contexts with invalid regex fails + Given an actor context named "docs" exists with messages + When I run actor context list "[invalid(" + Then the actor context list command should fail with exit code 1 + + Scenario: List empty when no contexts exist + When I run actor context list + Then the actor context list command should succeed + + Scenario: List outputs JSON format + Given an actor context named "docs" exists with messages + And an actor context named "notes" exists with messages + When I run actor context list --format json + Then the actor context list command should succeed + And the output should contain valid JSON with key "context_list" + And the output JSON key "context_list" should contain keys "count, contexts" + + Scenario: List outputs YAML format + Given an actor context named "docs" exists with messages + When I run actor context list --format yaml + Then the actor context list command should succeed + + # ── context show ──────────────────────────────────────────── + + Scenario: Show a named actor context + Given an actor context named "docs" exists with messages + When I run actor context show "docs" + Then the actor context show command should succeed + + Scenario: Show a named actor context outputs JSON format + Given an actor context named "docs" exists with messages + When I run actor context show "docs" --format json + Then the actor context show command should succeed + And the output should contain valid JSON with key "context_show" + And the output JSON key "context_show" should contain keys "name, message_count, total_size_kb, estimated_tokens, created_at" + + Scenario: Show a non-existent context fails + When I run actor context show "nonexistent" + Then the actor context show command should fail with exit code 1 diff --git a/features/steps/actor_context_cmds_steps.py b/features/steps/actor_context_cmds_steps.py index 16925aa17..671ccdc43 100644 --- a/features/steps/actor_context_cmds_steps.py +++ b/features/steps/actor_context_cmds_steps.py @@ -1,5 +1,5 @@ # pyright: reportRedeclaration=false -"""Step definitions for actor context remove/export/import commands.""" +"""Step definitions for actor context list, clear, show, remove, export, and import commands.""" from __future__ import annotations @@ -576,3 +576,117 @@ def step_roundtrip_messages(context): assert orig["content"] == imp["content"], ( f"Content mismatch: {orig['content']!r} vs {imp['content']!r}" ) + + +# --------------------------------------------------------------------------- +# When — list +# --------------------------------------------------------------------------- + + +@when("I run actor context list") +def step_list_all(context): + context.result = context.runner.invoke( + actor_context_app, + ["list", "--context-dir", str(context.context_dir)], + ) + + +@when('I run actor context list "{regex}"') +def step_list_regex(context, regex): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + regex, + "--context-dir", + str(context.context_dir), + ], + ) + + +@when('I run actor context list --format "{fmt}"') +def step_list_format(context, fmt): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + "--context-dir", + str(context.context_dir), + "--format", + fmt, + ], + ) + + +# --------------------------------------------------------------------------- +# When — show +# --------------------------------------------------------------------------- + + +@when('I run actor context show "{name}"') +def step_show(context, name): + context.result = context.runner.invoke( + actor_context_app, + [ + "show", + name, + "--context-dir", + str(context.context_dir), + ], + ) + + +@when('I run actor context show "{name}" --format "{fmt}"') +def step_show_format(context, name, fmt): + context.result = context.runner.invoke( + actor_context_app, + [ + "show", + name, + "--context-dir", + str(context.context_dir), + "--format", + fmt, + ], + ) + + +# --------------------------------------------------------------------------- +# Then — list / show success and failure assertions +# --------------------------------------------------------------------------- + + +@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 list command should fail with exit code 1") +def step_list_fail(context): + assert context.result.exit_code == 1, ( + f"Expected exit 1, got {context.result.exit_code}.\n" + f"stdout: {context.result.output}\n" + f"stderr: {getattr(context.result, 'stderr', '')}" + ) + + +@then("the actor context show command should succeed") +def step_show_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 show command should fail with exit code 1") +def step_show_fail(context): + assert context.result.exit_code == 1, ( + f"Expected exit 1, got {context.result.exit_code}.\n" + f"stdout: {context.result.output}\n" + f"stderr: {getattr(context.result, 'stderr', '')}" + ) diff --git a/src/cleveragents/cli/commands/actor_context.py b/src/cleveragents/cli/commands/actor_context.py index c90d33e49..f47b5400d 100644 --- a/src/cleveragents/cli/commands/actor_context.py +++ b/src/cleveragents/cli/commands/actor_context.py @@ -1,6 +1,7 @@ """Actor-scoped context management commands. -Implements ``agents actor context clear``, ``agents actor context remove``, +Implements ``agents actor context clear``, ``agents actor context list``, +``agents actor context remove``, ``agents actor context show``, ``agents actor context export``, and ``agents actor context import`` per the v3 specification. These commands manage named conversation contexts stored under ``~/.cleveragents/context/`` using the @@ -12,6 +13,7 @@ from __future__ import annotations import hashlib import json +import re import shutil from pathlib import Path from typing import Annotated, Any @@ -380,6 +382,212 @@ def context_clear( _render_output(data, fmt, rich_panels=panels, ok_message="Context cleared") +# --------------------------------------------------------------------------- +# New: list / show commands +# --------------------------------------------------------------------------- + + +@app.command("list") +def context_list( + regex: Annotated[ + str | None, + typer.Argument( + help="Optional regex pattern to filter context names", + ), + ] = None, + context_dir: Annotated[ + Path | None, + typer.Option( + "--context-dir", + help="Directory where contexts are stored", + resolve_path=True, + ), + ] = None, + fmt: Annotated[ + str, + typer.Option("--format", "-f", help=_FORMAT_HELP), + ] = "rich", +) -> None: + """List actor contexts with an optional regex filter. + + When a REGEX pattern is supplied only context names matching the pattern + are listed. Output can be rendered as a rich table, JSON, YAML, plain + text, color, or table format. + + Examples:: + + agents actor context list + agents actor context list "docs" + agents actor context list "^proj_" --format json + """ + base = _default_context_base(context_dir) + all_names = _list_context_names(base) + + if regex is not None: + try: + pattern = re.compile(regex) + except re.error as exc: + typer.echo(f"Error: Invalid regex: {exc}", err=True) + raise typer.Exit(code=1) from exc + names = [n for n in all_names if pattern.search(n)] + else: + names = list(all_names) + + if not names and regex is not None: + data: dict[str, Any] = { + "context_list": { + "count": 0, + "contexts": [], + "filter": regex, + } + } + console.print(format_output(data, fmt)) + return + + if not names: + typer.echo("No contexts found.") + return + + # Build per-context summary data + context_entries: list[dict[str, Any]] = [] + for cname in names: + ctx_mgr = ContextManager(cname, context_dir) + size_kb = _context_size_kb(ctx_mgr) + msg_count = len(ctx_mgr.messages) + created_at = ctx_mgr.metadata.get("created_at", "") + context_entries.append( + { + "name": cname, + "messages": msg_count, + "size_kb": round(size_kb, 1), + "created_at": created_at, + } + ) + + table_data: dict[str, Any] = { + "context_list": { + "count": len(names), + "contexts": context_entries, + **({"filter": regex} if regex is not None else {}), + } + } + + if fmt == OutputFormat.RICH.value or fmt == OutputFormat.TABLE.value: + from rich.table import Table as RichTable + + table = RichTable(title=f"Actor Contexts ({len(names)} total)") + table.add_column("Name", style="cyan") + table.add_column("Messages", style="green") + table.add_column("Size (KB)", style="magenta") + table.add_column("Created", style="yellow") + + for entry in context_entries: + table.add_row( + entry["name"], + str(entry["messages"]), + f'{entry["size_kb"]}', + entry["created_at"] or "", + ) + + console.print(table) + elif fmt == OutputFormat.PLAIN.value or fmt == OutputFormat.COLOR.value: + for cname in names: + typer.echo(cname) + else: + # json / yaml + console.print(format_output(table_data, fmt)) + + +@app.command("show") +def context_show( + name: Annotated[ + str, + typer.Argument(help="Context name to show"), + ], + context_dir: Annotated[ + Path | None, + typer.Option( + "--context-dir", + help="Directory where contexts are stored", + resolve_path=True, + ), + ] = None, + fmt: Annotated[ + str, + typer.Option("--format", "-f", help=_FORMAT_HELP), + ] = "rich", +) -> None: + """Show a summary of a named actor context. + + Displays message count, total storage size, estimated token usage, and + creation timestamp for the given context. Output supports rich, JSON, + YAML, plain, table, and color formats. + + Examples:: + + agents actor context show docs + agents actor context show docs --format json + """ + base = _default_context_base(context_dir) + if not (base / name).exists(): + typer.echo(f"Error: Context '{name}' does not exist.", err=True) + raise typer.Exit(code=1) + + ctx_mgr = ContextManager(name, context_dir) + size_kb = _context_size_kb(ctx_mgr) + msg_count = len(ctx_mgr.messages) + created_at = ctx_mgr.metadata.get("created_at", "") + + # Estimate tokens: rough rule of thumb is ~4 chars per token + total_chars = sum( + len(str(msg.get("content", ""))) for msg in ctx_mgr.messages + ) + estimated_tokens = round(total_chars / 4) + + data: dict[str, Any] = { + "context_show": { + "name": name, + "message_count": msg_count, + "total_size_kb": round(size_kb, 1), + "estimated_tokens": estimated_tokens, + "created_at": created_at, + } + } + + if fmt == OutputFormat.RICH.value: + panels = [ + ( + f"Context: {name}", + ( + f"[bold]Messages:[/bold] {msg_count}\n" + f"[bold]Size:[/bold] {round(size_kb, 1)} KB\n" + f"[bold]Est. Tokens:[/bold] {estimated_tokens}\n" + f"[bold]Created:[/bold] {created_at}" + ), + ) + ] + _render_output(data, fmt, rich_panels=panels, ok_message="Context summary") + elif fmt == OutputFormat.TABLE.value: + from rich.table import Table as RichTable + + table = RichTable(title=f"Context Summary: {name}") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="green") + table.add_row("Messages", str(msg_count)) + table.add_row("Size (KB)", f"{round(size_kb, 1)}") + table.add_row("Est. Tokens", str(estimated_tokens)) + table.add_row("Created", created_at) + console.print(table) + elif fmt == OutputFormat.PLAIN.value or fmt == OutputFormat.COLOR.value: + typer.echo(f"Context: {name}") + typer.echo(f"Messages: {msg_count}") + typer.echo(f"Size: {round(size_kb, 1)} KB") + typer.echo(f"Est. Tokens: {estimated_tokens}") + typer.echo(f"Created: {created_at}") + else: + console.print(format_output(data, fmt)) + + @app.command("export") def context_export( name: Annotated[ -- 2.52.0 From 542e076e5302171d2ded767b75aac9f371fd0913 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Wed, 13 May 2026 10:35:58 +0000 Subject: [PATCH 2/3] fix(cli): resolve AmbiguousStep in actor context show step definitions Replace two overlapping @when patterns (both with (name) kwargs) with a single @step pattern using optional {fmt} group to eliminate the Behave AmbiguousStep registration conflict that was causing CI unit_tests to fail. Co-changes: ruff auto-format applied to actor_context.py for style compliance. --- features/steps/actor_context_cmds_steps.py | 39 ++++++------------- .../cli/commands/actor_context.py | 6 +-- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/features/steps/actor_context_cmds_steps.py b/features/steps/actor_context_cmds_steps.py index 671ccdc43..61acf9a55 100644 --- a/features/steps/actor_context_cmds_steps.py +++ b/features/steps/actor_context_cmds_steps.py @@ -9,7 +9,7 @@ import tempfile from pathlib import Path from typing import Any -from behave import given, then, when +from behave import given, step, then, when from typer.testing import CliRunner from cleveragents.cli.commands.actor_context import app as actor_context_app @@ -623,32 +623,17 @@ def step_list_format(context, fmt): # --------------------------------------------------------------------------- -@when('I run actor context show "{name}"') -def step_show(context, name): - context.result = context.runner.invoke( - actor_context_app, - [ - "show", - name, - "--context-dir", - str(context.context_dir), - ], - ) - - -@when('I run actor context show "{name}" --format "{fmt}"') -def step_show_format(context, name, fmt): - context.result = context.runner.invoke( - actor_context_app, - [ - "show", - name, - "--context-dir", - str(context.context_dir), - "--format", - fmt, - ], - ) +@step('I run actor context show "{name}"{fmt}') +def step_show(context, name, fmt=None): + args = [ + "show", + name, + "--context-dir", + str(context.context_dir), + ] + if fmt is not None: + args.extend(["--format", fmt]) + context.result = context.runner.invoke(actor_context_app, args) # --------------------------------------------------------------------------- diff --git a/src/cleveragents/cli/commands/actor_context.py b/src/cleveragents/cli/commands/actor_context.py index f47b5400d..758939318 100644 --- a/src/cleveragents/cli/commands/actor_context.py +++ b/src/cleveragents/cli/commands/actor_context.py @@ -485,7 +485,7 @@ def context_list( table.add_row( entry["name"], str(entry["messages"]), - f'{entry["size_kb"]}', + f"{entry['size_kb']}", entry["created_at"] or "", ) @@ -539,9 +539,7 @@ def context_show( created_at = ctx_mgr.metadata.get("created_at", "") # Estimate tokens: rough rule of thumb is ~4 chars per token - total_chars = sum( - len(str(msg.get("content", ""))) for msg in ctx_mgr.messages - ) + total_chars = sum(len(str(msg.get("content", ""))) for msg in ctx_mgr.messages) estimated_tokens = round(total_chars / 4) data: dict[str, Any] = { -- 2.52.0 From 85c93e446f6e3da656a8849c5cd3db2c02a9c0bc Mon Sep 17 00:00:00 2001 From: CleverAgents Bot Date: Thu, 18 Jun 2026 12:06:49 -0400 Subject: [PATCH 3/3] test(actor-context): match list and show step text --- features/steps/actor_context_cmds_steps.py | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/features/steps/actor_context_cmds_steps.py b/features/steps/actor_context_cmds_steps.py index 61acf9a55..0150e4dca 100644 --- a/features/steps/actor_context_cmds_steps.py +++ b/features/steps/actor_context_cmds_steps.py @@ -9,7 +9,7 @@ import tempfile from pathlib import Path from typing import Any -from behave import given, step, then, when +from behave import given, then, when from typer.testing import CliRunner from cleveragents.cli.commands.actor_context import app as actor_context_app @@ -604,7 +604,7 @@ def step_list_regex(context, regex): ) -@when('I run actor context list --format "{fmt}"') +@when("I run actor context list --format {fmt}") def step_list_format(context, fmt): context.result = context.runner.invoke( actor_context_app, @@ -623,19 +623,32 @@ def step_list_format(context, fmt): # --------------------------------------------------------------------------- -@step('I run actor context show "{name}"{fmt}') -def step_show(context, name, fmt=None): +@when('I run actor context show "{name}"') +def step_show(context, name): args = [ "show", name, "--context-dir", str(context.context_dir), ] - if fmt is not None: - args.extend(["--format", fmt]) context.result = context.runner.invoke(actor_context_app, args) +@when('I run actor context show "{name}" --format {fmt}') +def step_show_format(context, name, fmt): + context.result = context.runner.invoke( + actor_context_app, + [ + "show", + name, + "--context-dir", + str(context.context_dir), + "--format", + fmt, + ], + ) + + # --------------------------------------------------------------------------- # Then — list / show success and failure assertions # --------------------------------------------------------------------------- -- 2.52.0