feat(cli): add interactive repl
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 19s
CI / build (pull_request) Successful in 24s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 43s
CI / integration_tests (pull_request) Successful in 2m37s
CI / unit_tests (pull_request) Successful in 6m12s
CI / docker (pull_request) Successful in 1m1s
CI / benchmark-regression (pull_request) Successful in 15m33s
CI / coverage (pull_request) Successful in 21m54s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 19s
CI / build (pull_request) Successful in 24s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 43s
CI / integration_tests (pull_request) Successful in 2m37s
CI / unit_tests (pull_request) Successful in 6m12s
CI / docker (pull_request) Successful in 1m1s
CI / benchmark-regression (pull_request) Successful in 15m33s
CI / coverage (pull_request) Successful in 21m54s
Implement `agents repl` command providing an interactive read-eval-print loop that dispatches to existing CLI commands without the `agents` prefix. Features: - Command dispatch via existing Typer app - readline history with --no-history opt-out (default: ~/.cleveragents/history) - Tab-completion for all CLI commands and built-in commands - !! last-command repetition - Prompt context from CLEVERAGENTS_PROJECT / CLEVERAGENTS_PLAN env vars - Ctrl+C (continue) and Ctrl+D (exit) signal handling - Multi-line input with \ continuation - :help and :exit/:quit built-in commands Test coverage: - 15 Behave scenarios (features/repl.feature) - 10 Robot Framework smoke tests (robot/repl_smoke.robot) - ASV benchmark suite (benchmarks/repl_bench.py) - All nox sessions pass: lint, typecheck, unit_tests, integration_tests - Coverage: 97.2% (threshold: 97%)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Benchmarks for CleverAgents REPL startup and helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
try:
|
||||
from cleveragents.cli.commands.repl import (
|
||||
_get_prompt_context,
|
||||
_setup_completer,
|
||||
dispatch_command,
|
||||
run_repl,
|
||||
)
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
from cleveragents.cli.commands.repl import (
|
||||
_get_prompt_context,
|
||||
_setup_completer,
|
||||
dispatch_command,
|
||||
run_repl,
|
||||
)
|
||||
|
||||
|
||||
class _FakeInput:
|
||||
"""Feed predetermined lines then raise ``EOFError``."""
|
||||
|
||||
def __init__(self, lines: list[str]) -> None:
|
||||
self._lines = list(lines)
|
||||
self._idx = 0
|
||||
|
||||
def __call__(self, prompt: str = "") -> str:
|
||||
if self._idx >= len(self._lines):
|
||||
raise EOFError
|
||||
line = self._lines[self._idx]
|
||||
self._idx += 1
|
||||
return line
|
||||
|
||||
|
||||
def _run_repl_silent(lines: list[str]) -> int:
|
||||
"""Run REPL with mocked console and input; return exit code."""
|
||||
from rich.console import Console
|
||||
|
||||
buf = StringIO()
|
||||
console = Console(file=buf, no_color=True, width=200)
|
||||
fake = _FakeInput(lines)
|
||||
with (
|
||||
patch("cleveragents.cli.commands.repl._console", console),
|
||||
patch("builtins.input", fake),
|
||||
):
|
||||
return run_repl(no_history=True)
|
||||
|
||||
|
||||
class TimeSuite:
|
||||
"""Measure REPL startup and helper latency."""
|
||||
|
||||
def time_prompt_context_default(self) -> None:
|
||||
"""Benchmark ``_get_prompt_context`` with no env vars."""
|
||||
old_proj = os.environ.pop("CLEVERAGENTS_PROJECT", None)
|
||||
old_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)
|
||||
try:
|
||||
_get_prompt_context()
|
||||
finally:
|
||||
if old_proj is not None:
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
|
||||
if old_plan is not None:
|
||||
os.environ["CLEVERAGENTS_PLAN"] = old_plan
|
||||
|
||||
def time_prompt_context_with_vars(self) -> None:
|
||||
"""Benchmark ``_get_prompt_context`` with project and plan set."""
|
||||
old_proj = os.environ.get("CLEVERAGENTS_PROJECT")
|
||||
old_plan = os.environ.get("CLEVERAGENTS_PLAN")
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = "bench-proj"
|
||||
os.environ["CLEVERAGENTS_PLAN"] = "bench-plan"
|
||||
try:
|
||||
_get_prompt_context()
|
||||
finally:
|
||||
if old_proj is None:
|
||||
os.environ.pop("CLEVERAGENTS_PROJECT", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
|
||||
if old_plan is None:
|
||||
os.environ.pop("CLEVERAGENTS_PLAN", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_PLAN"] = old_plan
|
||||
|
||||
def time_setup_completer(self) -> None:
|
||||
"""Benchmark installing the readline completer."""
|
||||
_setup_completer()
|
||||
|
||||
def time_repl_immediate_eof(self) -> None:
|
||||
"""Benchmark REPL startup → immediate EOF exit."""
|
||||
_run_repl_silent([])
|
||||
|
||||
def time_repl_help_then_exit(self) -> None:
|
||||
"""Benchmark REPL: ``:help`` then exit."""
|
||||
_run_repl_silent([":help"])
|
||||
|
||||
def time_dispatch_version(self) -> None:
|
||||
"""Benchmark ``dispatch_command(["--version"])``."""
|
||||
dispatch_command(["--version"])
|
||||
@@ -0,0 +1,99 @@
|
||||
# Interactive REPL
|
||||
|
||||
The `agents repl` command starts an interactive read-eval-print loop that
|
||||
lets you run any CleverAgents CLI command without the leading `agents`
|
||||
prefix.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agents repl
|
||||
```
|
||||
|
||||
Once inside the REPL you can type commands directly:
|
||||
|
||||
```
|
||||
agents> plan list
|
||||
agents> session create --actor openai/gpt-4
|
||||
agents> config get log_level
|
||||
```
|
||||
|
||||
## Built-in commands
|
||||
|
||||
| Command | Description |
|
||||
|----------------|------------------------------------|
|
||||
| `:help` | Show available commands |
|
||||
| `:exit` | Exit the REPL (same as Ctrl+D) |
|
||||
| `:quit` | Alias for `:exit` |
|
||||
| `!!` | Repeat the last command |
|
||||
|
||||
## Command-line options
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------------------|----------------------------------|------------------------------------|
|
||||
| `--no-history` | `False` | Disable history loading/saving |
|
||||
| `--history-path` | `~/.cleveragents/history` | Custom history file location |
|
||||
|
||||
## Features
|
||||
|
||||
### Tab-completion
|
||||
|
||||
Press **Tab** to complete top-level command names. All CLI commands and
|
||||
built-in REPL commands are available as completion candidates.
|
||||
|
||||
### Command repetition
|
||||
|
||||
Type `!!` and press Enter to repeat the previous command. The repeated
|
||||
command is printed before execution so you can see what ran.
|
||||
|
||||
### Multi-line input
|
||||
|
||||
End a line with `\` to continue on the next line. The REPL shows a `...`
|
||||
continuation prompt. All lines are joined with a space before dispatch.
|
||||
|
||||
```
|
||||
agents> plan create \
|
||||
... --name "My Plan" \
|
||||
... --project demo
|
||||
```
|
||||
|
||||
### Prompt context
|
||||
|
||||
The prompt reflects the active project and plan when the environment
|
||||
variables `CLEVERAGENTS_PROJECT` and `CLEVERAGENTS_PLAN` are set:
|
||||
|
||||
```
|
||||
agents (myproject/active-plan)> plan status
|
||||
```
|
||||
|
||||
### Signal handling
|
||||
|
||||
| Signal | Behaviour |
|
||||
|----------|-------------------------------------------|
|
||||
| Ctrl+C | Cancels the current input; stays in REPL |
|
||||
| Ctrl+D | Exits the REPL cleanly |
|
||||
|
||||
## History
|
||||
|
||||
By default the REPL persists command history to
|
||||
`~/.cleveragents/history`. The file is created automatically on first
|
||||
use. History survives across sessions and is limited to 10 000 entries.
|
||||
|
||||
### Privacy considerations
|
||||
|
||||
The history file is stored in the user's home directory with default
|
||||
filesystem permissions. It may contain sensitive information such as
|
||||
project names, plan names, and configuration values passed on the
|
||||
command line. If this is a concern:
|
||||
|
||||
* Use `--no-history` to disable persistence entirely.
|
||||
* Periodically delete or truncate `~/.cleveragents/history`.
|
||||
* Ensure the directory permissions on `~/.cleveragents/` are
|
||||
restricted (e.g. `chmod 700`).
|
||||
|
||||
## Running the REPL smoke tests
|
||||
|
||||
```bash
|
||||
nox -e unit_tests -- features/repl.feature
|
||||
nox -e integration_tests # includes robot/repl_smoke.robot
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
Feature: Interactive REPL
|
||||
The `agents repl` command provides an interactive session for dispatching
|
||||
CLI commands without the leading `agents` prefix.
|
||||
|
||||
Scenario: REPL dispatches version command
|
||||
Given the REPL is started
|
||||
When I send the input "version"
|
||||
Then the REPL output should contain "CleverAgents"
|
||||
And the REPL exit code should be 0
|
||||
|
||||
Scenario: REPL handles :exit command
|
||||
Given the REPL is started
|
||||
When I send the input ":exit"
|
||||
Then the REPL should exit cleanly
|
||||
|
||||
Scenario: REPL handles :quit command
|
||||
Given the REPL is started
|
||||
When I send the input ":quit"
|
||||
Then the REPL should exit cleanly
|
||||
|
||||
Scenario: REPL handles :help command
|
||||
Given the REPL is started
|
||||
When I send the input ":help"
|
||||
Then the REPL output should contain "Built-in commands"
|
||||
And the REPL output should contain ":exit"
|
||||
|
||||
Scenario: REPL handles unknown command gracefully
|
||||
Given the REPL is started
|
||||
When I send the input "nonexistent-cmd"
|
||||
Then the REPL output should contain "Error"
|
||||
|
||||
Scenario: REPL repeats last command with !!
|
||||
Given the REPL is started
|
||||
When I send the input "version"
|
||||
And I send the input "!!"
|
||||
Then the REPL output should contain "CleverAgents"
|
||||
|
||||
Scenario: REPL reports no previous command for !!
|
||||
Given the REPL is started
|
||||
When I send the input "!!"
|
||||
Then the REPL output should contain "No previous command"
|
||||
|
||||
Scenario: REPL supports multi-line input
|
||||
Given the REPL is started
|
||||
When I send a multi-line version command
|
||||
Then the REPL output should contain "CleverAgents"
|
||||
|
||||
Scenario: REPL with history disabled
|
||||
Given the REPL is started with --no-history
|
||||
When I send the input "version"
|
||||
Then the REPL output should contain "CleverAgents"
|
||||
And no history file should be written
|
||||
|
||||
Scenario: REPL with history enabled writes history file
|
||||
Given the REPL is started with a custom history path
|
||||
When I send the input "version"
|
||||
And the REPL exits
|
||||
Then the history file should exist
|
||||
|
||||
Scenario: REPL shows prompt context from environment
|
||||
Given the environment variable "CLEVERAGENTS_PROJECT" is set to "myproj"
|
||||
And the environment variable "CLEVERAGENTS_PLAN" is set to "myplan"
|
||||
When I query the REPL prompt context
|
||||
Then the prompt should contain "myproj"
|
||||
And the prompt should contain "myplan"
|
||||
|
||||
Scenario: REPL handles Ctrl-C gracefully
|
||||
Given the REPL is started
|
||||
When I simulate a KeyboardInterrupt during input
|
||||
Then the REPL should continue running
|
||||
|
||||
Scenario: REPL handles Ctrl-D for clean exit
|
||||
Given the REPL is started
|
||||
When I simulate an EOFError during input
|
||||
Then the REPL should exit cleanly
|
||||
|
||||
Scenario: REPL handles empty input
|
||||
Given the REPL is started
|
||||
When I send an empty input line
|
||||
Then the REPL should continue without error
|
||||
|
||||
Scenario: REPL dispatches config list command
|
||||
Given the REPL is started
|
||||
When I send the input "config list --format json"
|
||||
Then the REPL exit code should be 0
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Behave step implementations for the REPL feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.cli.commands.repl import (
|
||||
_get_prompt_context,
|
||||
run_repl,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _FakeInput:
|
||||
"""Simulate user input for the REPL by feeding lines then raising EOFError."""
|
||||
|
||||
def __init__(self, lines: list[str]) -> None:
|
||||
self._lines = list(lines)
|
||||
self._index = 0
|
||||
|
||||
def __call__(self, prompt: str = "") -> str:
|
||||
if self._index >= len(self._lines):
|
||||
raise EOFError
|
||||
line = self._lines[self._index]
|
||||
self._index += 1
|
||||
if line == "__KEYBOARD_INTERRUPT__":
|
||||
raise KeyboardInterrupt
|
||||
return line
|
||||
|
||||
|
||||
def _run_repl_with_input(
|
||||
lines: list[str],
|
||||
*,
|
||||
no_history: bool = False,
|
||||
history_path: Path | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""Run the REPL with simulated input and capture console output."""
|
||||
from io import StringIO
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
buf = StringIO()
|
||||
console = Console(file=buf, no_color=True, width=200)
|
||||
|
||||
kw: dict[str, object] = {"no_history": no_history}
|
||||
if history_path is not None:
|
||||
kw["history_path"] = history_path
|
||||
|
||||
fake = _FakeInput(lines)
|
||||
with (
|
||||
patch("cleveragents.cli.commands.repl._console", console),
|
||||
patch("builtins.input", fake),
|
||||
):
|
||||
code = run_repl(**kw) # type: ignore[arg-type]
|
||||
|
||||
return code, buf.getvalue()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Given
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the REPL is started")
|
||||
def step_repl_started(context: Context) -> None:
|
||||
context.repl_lines = []
|
||||
context.repl_output = ""
|
||||
context.repl_exit_code = 0
|
||||
context.repl_no_history = True # default to no history in tests
|
||||
context.repl_history_path = None
|
||||
|
||||
|
||||
@given("the REPL is started with --no-history")
|
||||
def step_repl_started_no_history(context: Context) -> None:
|
||||
context.repl_lines = []
|
||||
context.repl_output = ""
|
||||
context.repl_exit_code = 0
|
||||
context.repl_no_history = True
|
||||
context.repl_history_path = None
|
||||
|
||||
|
||||
@given("the REPL is started with a custom history path")
|
||||
def step_repl_started_custom_history(context: Context) -> None:
|
||||
context.repl_lines = []
|
||||
context.repl_output = ""
|
||||
context.repl_exit_code = 0
|
||||
context.repl_no_history = False
|
||||
tmpdir = Path(tempfile.mkdtemp())
|
||||
context.repl_history_path = tmpdir / "test_history"
|
||||
context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send the input "{text}"')
|
||||
def step_send_input(context: Context, text: str) -> None:
|
||||
context.repl_lines.append(text)
|
||||
# Execute REPL with accumulated lines
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=context.repl_no_history,
|
||||
history_path=context.repl_history_path,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
@when("I send a multi-line version command")
|
||||
def step_send_multiline_version(context: Context) -> None:
|
||||
# Simulate typing ``version \`` followed by an empty continuation line
|
||||
context.repl_lines.append("version \\")
|
||||
context.repl_lines.append("")
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=context.repl_no_history,
|
||||
history_path=context.repl_history_path,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
@when("I send an empty input line")
|
||||
def step_send_empty_input(context: Context) -> None:
|
||||
context.repl_lines.append("")
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=context.repl_no_history,
|
||||
history_path=context.repl_history_path,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
@when("the REPL exits")
|
||||
def step_repl_exits(context: Context) -> None:
|
||||
# Re-run with the accumulated lines (EOFError ends the REPL)
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=context.repl_no_history,
|
||||
history_path=context.repl_history_path,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
@when("I query the REPL prompt context")
|
||||
def step_query_prompt(context: Context) -> None:
|
||||
context.repl_prompt = _get_prompt_context()
|
||||
|
||||
|
||||
@when("I simulate a KeyboardInterrupt during input")
|
||||
def step_simulate_ctrl_c(context: Context) -> None:
|
||||
# Send a keyboard interrupt marker, then :exit
|
||||
context.repl_lines = ["__KEYBOARD_INTERRUPT__", ":exit"]
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=True,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
@when("I simulate an EOFError during input")
|
||||
def step_simulate_ctrl_d(context: Context) -> None:
|
||||
# Empty lines list → immediate EOFError → clean exit
|
||||
context.repl_lines = []
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=True,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the REPL output should contain "{text}"')
|
||||
def step_output_contains(context: Context, text: str) -> None:
|
||||
assert text in context.repl_output, (
|
||||
f"Expected '{text}' in output but got:\n{context.repl_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the REPL exit code should be {code:d}")
|
||||
def step_exit_code(context: Context, code: int) -> None:
|
||||
assert context.repl_exit_code == code, (
|
||||
f"Expected exit code {code} but got {context.repl_exit_code}"
|
||||
)
|
||||
|
||||
|
||||
@then("the REPL should exit cleanly")
|
||||
def step_exit_cleanly(context: Context) -> None:
|
||||
# A clean exit means exit code 0
|
||||
if not hasattr(context, "repl_exit_code") or context.repl_exit_code is None:
|
||||
# Run with empty input to trigger EOFError → clean exit
|
||||
code, output = _run_repl_with_input(
|
||||
context.repl_lines,
|
||||
no_history=True,
|
||||
)
|
||||
context.repl_exit_code = code
|
||||
context.repl_output = output
|
||||
assert context.repl_exit_code == 0, (
|
||||
f"Expected clean exit (code 0) but got {context.repl_exit_code}"
|
||||
)
|
||||
|
||||
|
||||
@then("the REPL should continue running")
|
||||
def step_repl_continues(context: Context) -> None:
|
||||
# After Ctrl+C the REPL should still exit cleanly (via :exit or EOFError)
|
||||
assert context.repl_exit_code == 0, (
|
||||
f"REPL did not continue after interrupt (exit code {context.repl_exit_code})"
|
||||
)
|
||||
|
||||
|
||||
@then("the REPL should continue without error")
|
||||
def step_repl_continues_no_error(context: Context) -> None:
|
||||
assert context.repl_exit_code == 0
|
||||
|
||||
|
||||
@then("no history file should be written")
|
||||
def step_no_history_file(context: Context) -> None:
|
||||
if context.repl_history_path is not None:
|
||||
assert not context.repl_history_path.exists(), (
|
||||
f"History file should not exist but found: {context.repl_history_path}"
|
||||
)
|
||||
|
||||
|
||||
@then("the history file should exist")
|
||||
def step_history_exists(context: Context) -> None:
|
||||
assert context.repl_history_path is not None
|
||||
assert context.repl_history_path.exists(), (
|
||||
f"History file should exist at {context.repl_history_path}"
|
||||
)
|
||||
|
||||
|
||||
@then('the prompt should contain "{text}"')
|
||||
def step_prompt_contains(context: Context, text: str) -> None:
|
||||
assert text in context.repl_prompt, (
|
||||
f"Expected '{text}' in prompt '{context.repl_prompt}'"
|
||||
)
|
||||
+22
-22
@@ -6502,28 +6502,28 @@ Deferred items remain planned but are not part of the 30-day MVP scope.
|
||||
- [ ] Git [Hamza]: `git push -u origin feature/m7-post-server`
|
||||
- [ ] Forgejo PR [Hamza]: Open PR from `feature/m7-post-server` to `master` with a suitable and thorough description
|
||||
|
||||
- [ ] **COMMIT (Owner: Rui | Group: POST.repl | Branch: feature/m7-post-repl | Planned: Day 36 | Expected: Day 44) - Commit message: "feat(cli): add interactive repl"**
|
||||
- [ ] Git [Rui]: `git checkout master`
|
||||
- [ ] Git [Rui]: `git pull origin master`
|
||||
- [ ] Git [Rui]: `git checkout -b feature/m7-post-repl`
|
||||
- [ ] Git [Rui]: `git fetch origin && git merge origin/master` (run before final tests and before commit)
|
||||
- [ ] Code [Rui]: Implement `agents repl` command that dispatches to existing CLI commands with shared config handling.
|
||||
- [ ] Code [Rui]: Add history support with opt-out (`--no-history`) and default path `~/.cleveragents/history`.
|
||||
- [ ] Code [Rui]: Add tab-completion for top-level commands and last command repetition (`!!`).
|
||||
- [ ] Code [Rui]: Add prompt context (active project/plan) and graceful handling of Ctrl+C/Ctrl+D.
|
||||
- [ ] Code [Rui]: Add multi-line input support for long commands and quoted strings.
|
||||
- [ ] Code [Rui]: Add `:help` and `:exit` built-in REPL commands.
|
||||
- [ ] Docs [Rui]: Add REPL usage guide with supported commands and exit behavior.
|
||||
- [ ] Docs [Rui]: Document history file location and privacy considerations.
|
||||
- [ ] Tests (Behave) [Rui]: Add REPL behavior scenarios (history on/off, unknown command, exit).
|
||||
- [ ] Tests (Robot) [Rui]: Add REPL smoke tests for command dispatch.
|
||||
- [ ] Tests (ASV) [Rui]: Add `benchmarks/repl_bench.py` for REPL startup baseline.
|
||||
- [ ] Quality [Rui]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [ ] Quality [Rui]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [ ] Git [Rui]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [ ] Git [Rui]: `git commit -m "feat(cli): add interactive repl"`
|
||||
- [ ] Git [Rui]: `git push -u origin feature/m7-post-repl`
|
||||
- [ ] Forgejo PR [Rui]: Open PR from `feature/m7-post-repl` to `master` with a suitable and thorough description
|
||||
- [X] **COMMIT (Owner: Rui | Group: POST.repl | Branch: feature/m7-post-repl | Planned: Day 36 | Expected: Day 44) - Commit message: "feat(cli): add interactive repl"**
|
||||
- [X] Git [Rui]: `git checkout master`
|
||||
- [X] Git [Rui]: `git pull origin master`
|
||||
- [X] Git [Rui]: `git checkout -b feature/m7-post-repl`
|
||||
- [X] Git [Rui]: `git fetch origin && git merge origin/master` (run before final tests and before commit)
|
||||
- [X] Code [Rui]: Implement `agents repl` command that dispatches to existing CLI commands with shared config handling.
|
||||
- [X] Code [Rui]: Add history support with opt-out (`--no-history`) and default path `~/.cleveragents/history`.
|
||||
- [X] Code [Rui]: Add tab-completion for top-level commands and last command repetition (`!!`).
|
||||
- [X] Code [Rui]: Add prompt context (active project/plan) and graceful handling of Ctrl+C/Ctrl+D.
|
||||
- [X] Code [Rui]: Add multi-line input support for long commands and quoted strings.
|
||||
- [X] Code [Rui]: Add `:help` and `:exit` built-in REPL commands.
|
||||
- [X] Docs [Rui]: Add REPL usage guide with supported commands and exit behavior.
|
||||
- [X] Docs [Rui]: Document history file location and privacy considerations.
|
||||
- [X] Tests (Behave) [Rui]: Add REPL behavior scenarios (history on/off, unknown command, exit).
|
||||
- [X] Tests (Robot) [Rui]: Add REPL smoke tests for command dispatch.
|
||||
- [X] Tests (ASV) [Rui]: Add `benchmarks/repl_bench.py` for REPL startup baseline.
|
||||
- [X] Quality [Rui]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [X] Quality [Rui]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [X] Git [Rui]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [X] Git [Rui]: `git commit -m "feat(cli): add interactive repl"`
|
||||
- [X] Git [Rui]: `git push -u origin feature/m7-post-repl`
|
||||
- [X] Forgejo PR [Rui]: Open PR from `feature/m7-post-repl` to `master` with a suitable and thorough description
|
||||
|
||||
- [ ] **COMMIT (Owner: Luis | Group: POST.auth | Branch: feature/m7-post-auth | Planned: Day 37 | Expected: Day 45) - Commit message: "feat(cli): add auth and team commands"**
|
||||
- [ ] Git [Luis]: `git checkout master`
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Helper script for repl_smoke.robot smoke tests.
|
||||
|
||||
Each function is a self-contained check used as a Robot Framework keyword.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure local source tree is importable
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.cli.commands.repl import ( # noqa: E402
|
||||
_get_prompt_context,
|
||||
_setup_completer,
|
||||
dispatch_command,
|
||||
run_repl,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fake input helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _FakeInput:
|
||||
"""Feed predetermined lines then raise ``EOFError``."""
|
||||
|
||||
def __init__(self, lines: list[str]) -> None:
|
||||
self._lines = list(lines)
|
||||
self._idx = 0
|
||||
|
||||
def __call__(self, prompt: str = "") -> str:
|
||||
if self._idx >= len(self._lines):
|
||||
raise EOFError
|
||||
line = self._lines[self._idx]
|
||||
self._idx += 1
|
||||
return line
|
||||
|
||||
|
||||
def _run(lines: list[str]) -> tuple[int, str]:
|
||||
from rich.console import Console
|
||||
|
||||
buf = StringIO()
|
||||
console = Console(file=buf, no_color=True, width=200)
|
||||
fake = _FakeInput(lines)
|
||||
with (
|
||||
patch("cleveragents.cli.commands.repl._console", console),
|
||||
patch("builtins.input", fake),
|
||||
):
|
||||
code = run_repl(no_history=True)
|
||||
return code, buf.getvalue()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Robot keywords
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_default_prompt() -> str:
|
||||
"""Return prompt context with no environment variables set."""
|
||||
old_proj = os.environ.pop("CLEVERAGENTS_PROJECT", None)
|
||||
old_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)
|
||||
try:
|
||||
return _get_prompt_context()
|
||||
finally:
|
||||
if old_proj is not None:
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
|
||||
if old_plan is not None:
|
||||
os.environ["CLEVERAGENTS_PLAN"] = old_plan
|
||||
|
||||
|
||||
def get_prompt_with_project(project: str) -> str:
|
||||
"""Return prompt context with CLEVERAGENTS_PROJECT set."""
|
||||
old = os.environ.get("CLEVERAGENTS_PROJECT")
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = project
|
||||
try:
|
||||
return _get_prompt_context()
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("CLEVERAGENTS_PROJECT", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = old
|
||||
|
||||
|
||||
def get_prompt_with_project_and_plan(project: str, plan: str) -> str:
|
||||
"""Return prompt context with both env vars set."""
|
||||
old_proj = os.environ.get("CLEVERAGENTS_PROJECT")
|
||||
old_plan = os.environ.get("CLEVERAGENTS_PLAN")
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = project
|
||||
os.environ["CLEVERAGENTS_PLAN"] = plan
|
||||
try:
|
||||
return _get_prompt_context()
|
||||
finally:
|
||||
if old_proj is None:
|
||||
os.environ.pop("CLEVERAGENTS_PROJECT", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
|
||||
if old_plan is None:
|
||||
os.environ.pop("CLEVERAGENTS_PLAN", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_PLAN"] = old_plan
|
||||
|
||||
|
||||
def dispatch_argv(*args: str) -> int:
|
||||
"""Call ``dispatch_command`` with the given argv tokens."""
|
||||
return dispatch_command(list(args))
|
||||
|
||||
|
||||
def run_repl_with_eof() -> int:
|
||||
"""Run the REPL with no input (immediate EOF)."""
|
||||
code, _ = _run([])
|
||||
return code
|
||||
|
||||
|
||||
def run_repl_with_help() -> str:
|
||||
"""Run the REPL, send ``:help`` then exit, return output."""
|
||||
_, output = _run([":help"])
|
||||
return output
|
||||
|
||||
|
||||
def run_repl_with_bang_bang() -> str:
|
||||
"""Run the REPL, send ``!!`` with no prior command, return output."""
|
||||
_, output = _run(["!!"])
|
||||
return output
|
||||
|
||||
|
||||
def verify_completer() -> bool:
|
||||
"""Verify that the tab completer is installed and recognises commands."""
|
||||
import readline
|
||||
|
||||
_setup_completer()
|
||||
# The completer should match ':help' when given ':'
|
||||
completer_func = readline.get_completer()
|
||||
if completer_func is None:
|
||||
return False
|
||||
match = completer_func(":", 0)
|
||||
return match is not None and match.startswith(":")
|
||||
@@ -0,0 +1,67 @@
|
||||
*** Settings ***
|
||||
Documentation REPL command smoke tests for CleverAgents
|
||||
Resource ${CURDIR}/common.resource
|
||||
Library Process
|
||||
Library OperatingSystem
|
||||
Library String
|
||||
Library Collections
|
||||
Library ${CURDIR}/helper_repl_smoke.py
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
|
||||
*** Test Cases ***
|
||||
REPL Help Flag Displays Usage
|
||||
[Documentation] ``agents repl --help`` shows the REPL help text
|
||||
${result}= Run Process ${PYTHON} -m cleveragents repl --help timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} interactive REPL session
|
||||
|
||||
REPL Module Is Importable
|
||||
[Documentation] The repl module can be imported without errors
|
||||
${result}= Run Process ${PYTHON} -c from cleveragents.cli.commands.repl import run_repl, dispatch_command timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
REPL Prompt Context Without Env Vars
|
||||
[Documentation] Default prompt context is ``agents> ``
|
||||
${prompt}= Get Default Prompt
|
||||
Should Be Equal ${prompt} agents>${{' '}}
|
||||
|
||||
REPL Prompt Context With Project
|
||||
[Documentation] Prompt contains the project name from env var
|
||||
${prompt}= Get Prompt With Project myproj
|
||||
Should Contain ${prompt} myproj
|
||||
|
||||
REPL Prompt Context With Project And Plan
|
||||
[Documentation] Prompt contains project/plan from env vars
|
||||
${prompt}= Get Prompt With Project And Plan myproj myplan
|
||||
Should Contain ${prompt} myproj
|
||||
Should Contain ${prompt} myplan
|
||||
|
||||
REPL Dispatch Version Command
|
||||
[Documentation] dispatch_command(["--version"]) returns 0
|
||||
${rc}= Dispatch Argv --version
|
||||
Should Be Equal As Integers ${rc} 0
|
||||
|
||||
REPL Run With Immediate EOF Exits Cleanly
|
||||
[Documentation] REPL with no input (immediate EOF) exits with code 0
|
||||
${rc}= Run Repl With Eof
|
||||
Should Be Equal As Integers ${rc} 0
|
||||
|
||||
REPL Help Builtin Shows Commands
|
||||
[Documentation] ``:help`` built-in prints available commands
|
||||
${output}= Run Repl With Help
|
||||
Should Contain ${output} Built-in commands
|
||||
Should Contain ${output} :exit
|
||||
|
||||
REPL No Previous Command Warning
|
||||
[Documentation] ``!!`` with no prior command shows a warning
|
||||
${output}= Run Repl With Bang Bang
|
||||
Should Contain ${output} No previous command
|
||||
|
||||
REPL Completer Is Installed
|
||||
[Documentation] Tab completer recognises known REPL commands
|
||||
${ok}= Verify Completer
|
||||
Should Be True ${ok}
|
||||
@@ -0,0 +1,293 @@
|
||||
"""Interactive REPL for CleverAgents.
|
||||
|
||||
Provides a read-eval-print loop that dispatches user input to existing
|
||||
CLI commands, with history, tab-completion, multi-line input, and
|
||||
prompt context showing the active project/plan.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import readline
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DEFAULT_HISTORY_DIR = Path.home() / ".cleveragents"
|
||||
_DEFAULT_HISTORY_PATH = _DEFAULT_HISTORY_DIR / "history"
|
||||
_MULTILINE_CONTINUATION = "\\"
|
||||
_LAST_COMMAND_TOKEN = "!!"
|
||||
|
||||
# The subset of first-position CLI commands exposed inside the REPL.
|
||||
# Mirrors the ``valid_cmds`` list in ``main.py`` minus the flags.
|
||||
_REPL_COMMANDS: list[str] = [
|
||||
"version",
|
||||
"info",
|
||||
"diagnostics",
|
||||
"init",
|
||||
"project",
|
||||
"context",
|
||||
"plan",
|
||||
"actor",
|
||||
"action",
|
||||
"resource",
|
||||
"skill",
|
||||
"lsp",
|
||||
"cleanup",
|
||||
"config",
|
||||
"session",
|
||||
"tool",
|
||||
"validation",
|
||||
"auto-debug",
|
||||
"automation-profile",
|
||||
"invariant",
|
||||
"tell",
|
||||
"build",
|
||||
"apply",
|
||||
"context-load",
|
||||
"context-add",
|
||||
]
|
||||
|
||||
_BUILTIN_COMMANDS: list[str] = [":help", ":exit", ":quit"]
|
||||
|
||||
_console = Console()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_prompt_context() -> str:
|
||||
"""Return a prompt string reflecting the active project/plan.
|
||||
|
||||
Reads ``CLEVERAGENTS_PROJECT`` and ``CLEVERAGENTS_PLAN`` from the
|
||||
environment. If neither is set the prompt falls back to a plain
|
||||
``agents``.
|
||||
"""
|
||||
project = os.environ.get("CLEVERAGENTS_PROJECT", "")
|
||||
plan = os.environ.get("CLEVERAGENTS_PLAN", "")
|
||||
parts: list[str] = []
|
||||
if project:
|
||||
parts.append(project)
|
||||
if plan:
|
||||
parts.append(plan)
|
||||
ctx = "/".join(parts) if parts else ""
|
||||
if ctx:
|
||||
return f"agents ({ctx})> "
|
||||
return "agents> "
|
||||
|
||||
|
||||
def _setup_history(history_path: Path) -> None:
|
||||
"""Load readline history from *history_path* (creating dirs as needed)."""
|
||||
history_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if history_path.exists():
|
||||
readline.read_history_file(str(history_path))
|
||||
readline.set_history_length(10_000)
|
||||
|
||||
|
||||
def _save_history(history_path: Path) -> None:
|
||||
"""Persist the current readline history to *history_path*."""
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(OSError):
|
||||
readline.write_history_file(str(history_path))
|
||||
|
||||
|
||||
def _setup_completer() -> None:
|
||||
"""Install a tab-completer for known commands and built-ins."""
|
||||
all_tokens = sorted(set(_REPL_COMMANDS + _BUILTIN_COMMANDS))
|
||||
|
||||
def completer(text: str, state: int) -> str | None:
|
||||
matches = [t for t in all_tokens if t.startswith(text)]
|
||||
if state < len(matches):
|
||||
return matches[state]
|
||||
return None
|
||||
|
||||
readline.set_completer(completer)
|
||||
readline.set_completer_delims(" \t\n")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
|
||||
def _read_multiline(initial: str, prompt_cont: str = "... ") -> str:
|
||||
"""Read continuation lines when *initial* ends with a backslash."""
|
||||
lines = [initial.rstrip(_MULTILINE_CONTINUATION)]
|
||||
while True:
|
||||
try:
|
||||
cont = input(prompt_cont)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if cont.endswith(_MULTILINE_CONTINUATION):
|
||||
lines.append(cont.rstrip(_MULTILINE_CONTINUATION))
|
||||
else:
|
||||
lines.append(cont)
|
||||
break
|
||||
return " ".join(lines)
|
||||
|
||||
|
||||
def _print_repl_help() -> None:
|
||||
"""Display available REPL commands and built-ins."""
|
||||
_console.print("[bold]CleverAgents Interactive REPL[/bold]\n")
|
||||
_console.print("[underline]Built-in commands[/underline]")
|
||||
_console.print(" :help Show this help message")
|
||||
_console.print(" :exit / :quit Exit the REPL")
|
||||
_console.print(" !! Repeat the last command")
|
||||
_console.print("")
|
||||
_console.print("[underline]CLI commands[/underline]")
|
||||
for cmd in _REPL_COMMANDS:
|
||||
_console.print(f" {cmd}")
|
||||
_console.print("\nType any CLI command without the leading 'agents' prefix.")
|
||||
_console.print("Use [bold]\\\\[/bold] at end of line for multi-line input.")
|
||||
|
||||
|
||||
def dispatch_command(argv: Sequence[str]) -> int:
|
||||
"""Invoke the main Typer CLI with the given argument vector.
|
||||
|
||||
Returns the integer exit code (0 for success).
|
||||
"""
|
||||
from cleveragents.cli.main import _register_subcommands
|
||||
from cleveragents.cli.main import app as root_app
|
||||
|
||||
_register_subcommands()
|
||||
try:
|
||||
root_app(list(argv), standalone_mode=False)
|
||||
except SystemExit as exc:
|
||||
return int(exc.code) if exc.code is not None else 0
|
||||
except KeyboardInterrupt:
|
||||
return 130
|
||||
except Exception as exc:
|
||||
_console.print(f"[red]Error:[/red] {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def run_repl(
|
||||
*,
|
||||
no_history: bool = False,
|
||||
history_path: Path = _DEFAULT_HISTORY_PATH,
|
||||
) -> int:
|
||||
"""Run the interactive REPL loop.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
no_history:
|
||||
When ``True`` readline history is neither loaded nor saved.
|
||||
history_path:
|
||||
Filesystem path used for persisting history between sessions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Exit code (always 0 for a clean exit).
|
||||
"""
|
||||
use_history = not no_history
|
||||
|
||||
if use_history:
|
||||
_setup_history(history_path)
|
||||
_setup_completer()
|
||||
|
||||
_console.print(
|
||||
"[bold green]CleverAgents REPL[/bold green] — "
|
||||
"type [bold]:help[/bold] for commands, "
|
||||
"[bold]:exit[/bold] to quit.\n"
|
||||
)
|
||||
|
||||
last_command: str = ""
|
||||
exit_code = 0
|
||||
|
||||
while True:
|
||||
prompt = _get_prompt_context()
|
||||
try:
|
||||
line = input(prompt)
|
||||
except EOFError:
|
||||
# Ctrl+D → clean exit
|
||||
_console.print("")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
# Ctrl+C → cancel current line, continue
|
||||
_console.print("")
|
||||
continue
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Multi-line continuation
|
||||
if line.endswith(_MULTILINE_CONTINUATION):
|
||||
line = _read_multiline(line)
|
||||
|
||||
# !! — repeat last command
|
||||
if line == _LAST_COMMAND_TOKEN:
|
||||
if not last_command:
|
||||
_console.print("[yellow]No previous command.[/yellow]")
|
||||
continue
|
||||
line = last_command
|
||||
_console.print(f"[dim]{line}[/dim]")
|
||||
|
||||
# Built-in commands
|
||||
if line in (":exit", ":quit"):
|
||||
break
|
||||
if line == ":help":
|
||||
_print_repl_help()
|
||||
continue
|
||||
|
||||
# Tokenise and dispatch
|
||||
try:
|
||||
argv = shlex.split(line)
|
||||
except ValueError as exc:
|
||||
_console.print(f"[red]Parse error:[/red] {exc}")
|
||||
continue
|
||||
|
||||
if not argv:
|
||||
continue
|
||||
|
||||
last_command = line
|
||||
exit_code = dispatch_command(argv)
|
||||
|
||||
if use_history:
|
||||
_save_history(history_path)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Typer entry-point (registered as ``agents repl`` in main.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_repl_app = typer.Typer(help="Interactive REPL for CleverAgents.")
|
||||
|
||||
|
||||
@_repl_app.callback(invoke_without_command=True)
|
||||
def repl_callback(
|
||||
no_history: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--no-history",
|
||||
help="Disable readline history loading and saving.",
|
||||
),
|
||||
] = False,
|
||||
history_path: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--history-path",
|
||||
help="Custom path for the history file.",
|
||||
),
|
||||
] = _DEFAULT_HISTORY_PATH,
|
||||
) -> None:
|
||||
"""Start an interactive REPL session."""
|
||||
# Only run the REPL when invoked directly (not during help generation)
|
||||
if sys.stdout.isatty() or os.environ.get("CLEVERAGENTS_FORCE_REPL"):
|
||||
raise SystemExit(run_repl(no_history=no_history, history_path=history_path))
|
||||
@@ -95,6 +95,7 @@ def _register_subcommands() -> None:
|
||||
validation,
|
||||
)
|
||||
from cleveragents.cli.commands.auto_debug import app as auto_debug_app
|
||||
from cleveragents.cli.commands.repl import _repl_app
|
||||
except Exception as exc: # pragma: no cover
|
||||
import traceback
|
||||
|
||||
@@ -182,6 +183,11 @@ def _register_subcommands() -> None:
|
||||
name="invariant",
|
||||
help="Manage invariant constraints for plan execution",
|
||||
)
|
||||
app.add_typer(
|
||||
_repl_app,
|
||||
name="repl",
|
||||
help="Start an interactive REPL session",
|
||||
)
|
||||
|
||||
_subcommands_registered = True
|
||||
|
||||
@@ -213,6 +219,7 @@ def _print_basic_help() -> None:
|
||||
typer.echo(" build Build the current plan")
|
||||
typer.echo(" apply Apply plan changes")
|
||||
typer.echo(" auto-debug Auto-debug operations")
|
||||
typer.echo(" repl Interactive REPL session")
|
||||
typer.echo(" version Show version")
|
||||
typer.echo("")
|
||||
typer.echo("Actors: set a default with 'agents actor set-default <name>'.")
|
||||
@@ -576,6 +583,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"auto-debug", # Auto-debug commands
|
||||
"automation-profile", # Automation profile management
|
||||
"invariant", # Invariant constraint management
|
||||
"repl", # Interactive REPL
|
||||
"tell", # Shortcut for plan tell
|
||||
"build", # Shortcut for plan build
|
||||
"apply", # Shortcut for plan apply
|
||||
|
||||
Reference in New Issue
Block a user