From 58793e1a5a268757fbe45137a659fc78c1f8c8b4 Mon Sep 17 00:00:00 2001 From: ShahadatTest Date: Mon, 2 Nov 2026 06:08:54 +0000 Subject: [PATCH] feat(cli): add agentplay list --sort before --limit Allow sorting built sessions by recent, cost, duration, calls, or errors, applying --limit after the sort so top-N queries work. Rows missing the sort field come last. Fixes #64 AI assistance: Cursor Cloud Agent helped implement and test this change. --- README.md | 3 ++- python/src/agentplay/cli.py | 29 ++++++++++++++++++++++++++++- python/tests/test_cli_output.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 832a25a..b43cd1b 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,8 @@ agentplay agents show which agents were found on this machine | `--agents claude-code,codex` | build, view, list | limit to these agents | | `--rebuild` | build, view | rebuild even unchanged sessions | | `--path-mode dirs_only` | build, view | see [Privacy](#privacy) | -| `--limit N` | build, view, list | newest N sessions | +| `--limit N` | build, view, list | newest N sessions (for list: after `--sort`) | +| `--sort cost` | list | order before `--limit`: `recent` (default), `cost`, `duration`, `calls`, `errors` | | `--no-titles` | build, view | don't keep local titles, and clear any already stored | | `--port 7317`, `--no-open`, `--no-build` | view | server options | | `--json` | build, list | machine-readable output | diff --git a/python/src/agentplay/cli.py b/python/src/agentplay/cli.py index 32ec765..354319e 100644 --- a/python/src/agentplay/cli.py +++ b/python/src/agentplay/cli.py @@ -239,6 +239,31 @@ def cmd_view(args: argparse.Namespace) -> int: # --------------------------------------------------------------------------- list +_LIST_SORT_FIELDS = { + "recent": "startedAt", + "cost": "costUsd", + "duration": "durationMs", + "calls": "calls", + "errors": "errors", +} + + +def _sort_list_rows(rows: list[dict], sort: str) -> list[dict]: + """Sort before --limit. Highest / newest first; rows missing the field last.""" + field = _LIST_SORT_FIELDS[sort] + if field == "startedAt": + present = [r for r in rows if r.get("startedAt")] + missing = [r for r in rows if not r.get("startedAt")] + present.sort(key=lambda r: r["startedAt"], reverse=True) + return present + missing + + def key(row: dict): + value = row.get(field) + return (value is None, 0 if value is None else -value) + + return sorted(rows, key=key) + + def cmd_list(args: argparse.Namespace) -> int: st = Store() agents = _agents(args.agents) @@ -261,7 +286,7 @@ def cmd_list(args: argparse.Namespace) -> int: if changed < since.timestamp(): continue rows.append(row) - rows = rows[: args.limit or 30] + rows = _sort_list_rows(rows, args.sort)[: args.limit or 30] if args.json: print(json.dumps(rows, indent=2)) return 0 @@ -544,6 +569,8 @@ def parser() -> argparse.ArgumentParser: ls = sub.add_parser("list", parents=[common], help="list built sessions") ls.add_argument("--agents", help=f"comma-separated subset of: {', '.join(discover.AGENTS)}") ls.add_argument("--since", help="only sessions changed since (30d, 12h, 2w, YYYY-MM-DD); missing modification times use start time") + ls.add_argument("--sort", default="recent", choices=list(_LIST_SORT_FIELDS), + help="order before --limit (default: recent)") ls.add_argument("--limit", type=int) ls.add_argument("--json", action="store_true") ls.set_defaults(fn=cmd_list) diff --git a/python/tests/test_cli_output.py b/python/tests/test_cli_output.py index 82aab87..7a9b778 100644 --- a/python/tests/test_cli_output.py +++ b/python/tests/test_cli_output.py @@ -219,6 +219,37 @@ def test_list_filters_independently(list_store, capsys, flags, expected): assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == expected +def test_list_sort_applies_before_limit(monkeypatch, tmp_path, capsys): + monkeypatch.setenv("AGENTPLAY_HOME", str(tmp_path)) + monkeypatch.setattr(cli, "_TTY", False) + st = cli.Store() + for sid, started, cost, duration, calls, errors in [ + ("cheap", "2026-10-01T00:00:00Z", 0.1, 60_000, 1, 0), + ("mid", "2026-10-02T00:00:00Z", 1.0, 120_000, 5, 1), + ("pricey", "2026-10-03T00:00:00Z", 9.0, 30_000, 2, 0), + ("long", "2026-09-01T00:00:00Z", 0.5, 600_000, 3, 0), + ("missing", "2026-10-04T00:00:00Z", None, None, None, None), + ]: + st.index[sid] = { + "sessionId": sid, "agentTool": "codex", "startedAt": started, + "costUsd": cost, "durationMs": duration, "calls": calls, "errors": errors, + } + st.save_index() + assert cli.main(["list", "--json", "--sort", "cost", "--limit", "3"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["pricey", "mid", "long"] + assert cli.main(["list", "--json", "--sort", "duration", "--limit", "2"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["long", "mid"] + assert cli.main(["list", "--json", "--sort", "calls", "--limit", "2"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["mid", "long"] + assert cli.main(["list", "--json", "--sort", "errors", "--limit", "2"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["mid", "pricey"] + # Missing numeric fields sort last; recent still newest-first. + assert cli.main(["list", "--json", "--sort", "cost"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)][-1] == "missing" + assert cli.main(["list", "--json", "--sort", "recent", "--limit", "2"]) == 0 + assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["missing", "pricey"] + + @pytest.mark.parametrize(("flags", "message"), [ (["--agents", "unknown"], "unknown agent"), (["--since", "yesterday"], "can't read --since"),