terminal-agent-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- terminal_agent/__init__.py +7 -0
- terminal_agent/__main__.py +7 -0
- terminal_agent/agent/__init__.py +9 -0
- terminal_agent/agent/loop.py +338 -0
- terminal_agent/checkpoints/__init__.py +9 -0
- terminal_agent/checkpoints/manager.py +151 -0
- terminal_agent/cli/commands/__init__.py +26 -0
- terminal_agent/cli/commands/checkpoint.py +76 -0
- terminal_agent/cli/commands/clean.py +170 -0
- terminal_agent/cli/commands/config_cmd.py +32 -0
- terminal_agent/cli/commands/diff.py +45 -0
- terminal_agent/cli/commands/doctor.py +136 -0
- terminal_agent/cli/commands/resume.py +109 -0
- terminal_agent/cli/commands/run.py +184 -0
- terminal_agent/cli/commands/setup.py +229 -0
- terminal_agent/cli/commands/status.py +62 -0
- terminal_agent/cli/commands/test.py +38 -0
- terminal_agent/cli/commands/trace.py +60 -0
- terminal_agent/cli/main.py +94 -0
- terminal_agent/cli/theme.py +55 -0
- terminal_agent/cli/ui.py +147 -0
- terminal_agent/config/__init__.py +30 -0
- terminal_agent/config/schema.py +100 -0
- terminal_agent/config/settings.py +85 -0
- terminal_agent/context/__init__.py +11 -0
- terminal_agent/context/engine.py +173 -0
- terminal_agent/context/ranker.py +87 -0
- terminal_agent/git/__init__.py +10 -0
- terminal_agent/git/adapter.py +219 -0
- terminal_agent/planner/__init__.py +10 -0
- terminal_agent/planner/contract.py +58 -0
- terminal_agent/planner/planner.py +30 -0
- terminal_agent/providers/__init__.py +30 -0
- terminal_agent/providers/anthropic_provider.py +141 -0
- terminal_agent/providers/base.py +57 -0
- terminal_agent/providers/detector.py +397 -0
- terminal_agent/providers/factory.py +29 -0
- terminal_agent/providers/gemini_provider.py +130 -0
- terminal_agent/providers/mock_provider.py +80 -0
- terminal_agent/providers/ollama_provider.py +124 -0
- terminal_agent/providers/openai_provider.py +124 -0
- terminal_agent/recovery/__init__.py +10 -0
- terminal_agent/recovery/classifier.py +64 -0
- terminal_agent/recovery/strategies.py +62 -0
- terminal_agent/sandbox/__init__.py +13 -0
- terminal_agent/sandbox/base.py +41 -0
- terminal_agent/sandbox/docker.py +129 -0
- terminal_agent/sandbox/local.py +129 -0
- terminal_agent/security/__init__.py +13 -0
- terminal_agent/security/classifier.py +151 -0
- terminal_agent/security/policy.py +51 -0
- terminal_agent/security/secrets.py +105 -0
- terminal_agent/session/__init__.py +33 -0
- terminal_agent/session/manager.py +195 -0
- terminal_agent/session/models.py +160 -0
- terminal_agent/telemetry/__init__.py +12 -0
- terminal_agent/telemetry/events.py +87 -0
- terminal_agent/telemetry/metrics.py +38 -0
- terminal_agent/tools/__init__.py +33 -0
- terminal_agent/tools/base.py +59 -0
- terminal_agent/tools/checkpoint_tools.py +72 -0
- terminal_agent/tools/command_tools.py +125 -0
- terminal_agent/tools/file_tools.py +340 -0
- terminal_agent/tools/git_tools.py +124 -0
- terminal_agent/tools/registry.py +109 -0
- terminal_agent/verifier/__init__.py +13 -0
- terminal_agent/verifier/assertions.py +115 -0
- terminal_agent/verifier/engine.py +125 -0
- terminal_agent/verifier/runners.py +80 -0
- terminal_agent_cli-0.1.0.dist-info/METADATA +438 -0
- terminal_agent_cli-0.1.0.dist-info/RECORD +74 -0
- terminal_agent_cli-0.1.0.dist-info/WHEEL +4 -0
- terminal_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- terminal_agent_cli-0.1.0.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""CLI clean command to prune old sessions, checkpoints, and telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from terminal_agent.checkpoints.manager import CheckpointManager
|
|
15
|
+
from terminal_agent.cli.theme import console
|
|
16
|
+
from terminal_agent.cli.ui import render_header
|
|
17
|
+
from terminal_agent.session.manager import SessionManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _dir_size_bytes(path: Path) -> int:
|
|
21
|
+
total = 0
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return 0
|
|
24
|
+
if path.is_file():
|
|
25
|
+
return path.stat().st_size
|
|
26
|
+
for root, _, files in os.walk(path):
|
|
27
|
+
for name in files:
|
|
28
|
+
fp = Path(root) / name
|
|
29
|
+
try:
|
|
30
|
+
total += fp.stat().st_size
|
|
31
|
+
except OSError:
|
|
32
|
+
continue
|
|
33
|
+
return total
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _format_mb(num_bytes: int) -> str:
|
|
37
|
+
return f"{num_bytes / (1024 * 1024):.2f} MB"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_older_than(path: Path, cutoff_ts: Optional[float]) -> bool:
|
|
41
|
+
if cutoff_ts is None:
|
|
42
|
+
return True
|
|
43
|
+
try:
|
|
44
|
+
return path.stat().st_mtime < cutoff_ts
|
|
45
|
+
except OSError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def collect_clean_targets(
|
|
50
|
+
working_dir: Path,
|
|
51
|
+
days: Optional[int],
|
|
52
|
+
purge_all: bool,
|
|
53
|
+
) -> List[Tuple[str, Path, int]]:
|
|
54
|
+
"""Return (kind, path, size_bytes) items that would be removed."""
|
|
55
|
+
cutoff = None
|
|
56
|
+
if not purge_all:
|
|
57
|
+
age = days if days is not None else 7
|
|
58
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=age)).timestamp()
|
|
59
|
+
|
|
60
|
+
root = working_dir / ".terminal_agent"
|
|
61
|
+
targets: List[Tuple[str, Path, int]] = []
|
|
62
|
+
|
|
63
|
+
sessions_dir = root / "sessions"
|
|
64
|
+
if sessions_dir.exists():
|
|
65
|
+
for json_file in sessions_dir.glob("*.json"):
|
|
66
|
+
if _is_older_than(json_file, cutoff):
|
|
67
|
+
targets.append(("session", json_file, _dir_size_bytes(json_file)))
|
|
68
|
+
|
|
69
|
+
checkpoints_dir = root / "checkpoints"
|
|
70
|
+
if checkpoints_dir.exists():
|
|
71
|
+
for entry in checkpoints_dir.iterdir():
|
|
72
|
+
if entry.is_dir() and _is_older_than(entry, cutoff):
|
|
73
|
+
targets.append(("checkpoint", entry, _dir_size_bytes(entry)))
|
|
74
|
+
|
|
75
|
+
for kind in ("telemetry", "traces"):
|
|
76
|
+
tdir = root / kind
|
|
77
|
+
if not tdir.exists():
|
|
78
|
+
continue
|
|
79
|
+
for entry in tdir.iterdir():
|
|
80
|
+
if entry.is_file() and _is_older_than(entry, cutoff):
|
|
81
|
+
targets.append((kind.rstrip("s"), entry, _dir_size_bytes(entry)))
|
|
82
|
+
elif entry.is_dir() and _is_older_than(entry, cutoff):
|
|
83
|
+
targets.append((kind.rstrip("s"), entry, _dir_size_bytes(entry)))
|
|
84
|
+
|
|
85
|
+
return targets
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def apply_deletes(working_dir: Path, days: Optional[int], purge_all: bool) -> List[Path]:
|
|
89
|
+
"""Delete matching artifacts. Uses manager helpers for sessions/checkpoints."""
|
|
90
|
+
age = None if purge_all else (days if days is not None else 7)
|
|
91
|
+
SessionManager(working_dir).delete_sessions_older_than(age)
|
|
92
|
+
CheckpointManager(working_dir).prune_checkpoints(age)
|
|
93
|
+
|
|
94
|
+
deleted: List[Path] = []
|
|
95
|
+
for kind, path, _size in collect_clean_targets(working_dir, days=days, purge_all=purge_all):
|
|
96
|
+
if kind in ("session", "checkpoint"):
|
|
97
|
+
deleted.append(path)
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
if path.is_dir():
|
|
101
|
+
shutil.rmtree(path)
|
|
102
|
+
elif path.exists():
|
|
103
|
+
path.unlink()
|
|
104
|
+
deleted.append(path)
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
return deleted
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clean_command(
|
|
111
|
+
all_data: bool = typer.Option(
|
|
112
|
+
False,
|
|
113
|
+
"--all",
|
|
114
|
+
help="Purge all sessions, checkpoints, and telemetry logs.",
|
|
115
|
+
),
|
|
116
|
+
days: int = typer.Option(
|
|
117
|
+
7,
|
|
118
|
+
"--days",
|
|
119
|
+
help="Purge items older than N days (ignored when --all is set).",
|
|
120
|
+
),
|
|
121
|
+
dry_run: bool = typer.Option(
|
|
122
|
+
False,
|
|
123
|
+
"--dry-run",
|
|
124
|
+
help="Show files and disk space that would be freed without deleting.",
|
|
125
|
+
),
|
|
126
|
+
working_dir: Optional[Path] = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
"--cwd",
|
|
129
|
+
"-C",
|
|
130
|
+
help="Target repository directory",
|
|
131
|
+
),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Prune old session, checkpoint, and telemetry artifacts."""
|
|
134
|
+
render_header()
|
|
135
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
136
|
+
root = target_dir / ".terminal_agent"
|
|
137
|
+
|
|
138
|
+
if not root.exists():
|
|
139
|
+
console.print("[agent.muted]No .terminal_agent directory found. Nothing to clean.[/agent.muted]")
|
|
140
|
+
raise typer.Exit(0)
|
|
141
|
+
|
|
142
|
+
targets = collect_clean_targets(target_dir, days=days, purge_all=all_data)
|
|
143
|
+
total_bytes = sum(size for _, _, size in targets)
|
|
144
|
+
|
|
145
|
+
if not targets:
|
|
146
|
+
console.print("[agent.muted]No session, checkpoint, or telemetry files to clean.[/agent.muted]")
|
|
147
|
+
raise typer.Exit(0)
|
|
148
|
+
|
|
149
|
+
table = Table(title="[bold agent.accent]CLEAN TARGETS[/bold agent.accent]", border_style="agent.border")
|
|
150
|
+
table.add_column("Kind", style="agent.text")
|
|
151
|
+
table.add_column("Path", style="agent.muted")
|
|
152
|
+
table.add_column("Size", justify="right")
|
|
153
|
+
for kind, path, size in targets:
|
|
154
|
+
try:
|
|
155
|
+
rel = path.relative_to(target_dir)
|
|
156
|
+
except ValueError:
|
|
157
|
+
rel = path
|
|
158
|
+
table.add_row(kind, str(rel), _format_mb(size))
|
|
159
|
+
console.print(table)
|
|
160
|
+
console.print(
|
|
161
|
+
f"[agent.accent]{len(targets)} item(s)[/agent.accent] — "
|
|
162
|
+
f"[agent.success]{_format_mb(total_bytes)}[/agent.success] would be freed."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if dry_run:
|
|
166
|
+
console.print("[agent.muted]Dry run: no files were deleted.[/agent.muted]")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
apply_deletes(target_dir, days=days, purge_all=all_data)
|
|
170
|
+
console.print("[agent.success]Cleanup complete. Directory structure preserved.[/agent.success]")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""CLI config command viewing and generating configuration templates."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
|
|
9
|
+
from terminal_agent.cli.theme import SYM_CHECK, console
|
|
10
|
+
from terminal_agent.cli.ui import render_header
|
|
11
|
+
from terminal_agent.config.settings import load_config, save_default_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def config_command(
|
|
15
|
+
init: bool = typer.Option(False, "--init", "-i", help="Generate default terminal-agent.config.yaml file"),
|
|
16
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""View active configuration or generate a default configuration file."""
|
|
19
|
+
render_header()
|
|
20
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
21
|
+
target_file = target_dir / "terminal-agent.config.yaml"
|
|
22
|
+
|
|
23
|
+
if init:
|
|
24
|
+
save_default_config(target_file)
|
|
25
|
+
console.print(f"{SYM_CHECK} [agent.success]Created default configuration at {target_file}[/agent.success]")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
config = load_config(target_file)
|
|
29
|
+
yaml_str = yaml.dump(config.model_dump(mode="json"), sort_keys=False, indent=2)
|
|
30
|
+
syntax = Syntax(yaml_str, "yaml", theme="monokai", line_numbers=True)
|
|
31
|
+
console.print(syntax)
|
|
32
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""CLI diff command displaying repository changes and stats."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
|
|
9
|
+
from terminal_agent.cli.theme import console
|
|
10
|
+
from terminal_agent.cli.ui import render_header
|
|
11
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def diff_command(
|
|
15
|
+
file_path: Optional[str] = typer.Argument(None, help="Specific file path to diff"),
|
|
16
|
+
stat_only: bool = typer.Option(False, "--stat", "-s", help="Show diffstat summary only"),
|
|
17
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""View current git diff and changes made by agent."""
|
|
20
|
+
render_header()
|
|
21
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
22
|
+
git_adapter = GitAdapter(target_dir)
|
|
23
|
+
|
|
24
|
+
if not git_adapter.is_git_repo():
|
|
25
|
+
console.print("[agent.error]Current directory is not a Git repository.[/agent.error]")
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
diff_stats = git_adapter.get_diff_stats()
|
|
29
|
+
diff_text = git_adapter.get_diff(file_path=file_path, stat=stat_only)
|
|
30
|
+
|
|
31
|
+
console.print(
|
|
32
|
+
f"[agent.accent]Changed Files ({diff_stats.files_changed}):[/agent.accent] "
|
|
33
|
+
f"[agent.success]+{diff_stats.insertions}[/agent.success] / [agent.error]-{diff_stats.deletions}[/agent.error] lines\n"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not diff_text.strip():
|
|
37
|
+
console.print("[agent.muted]No working tree modifications.[/agent.muted]")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if stat_only:
|
|
41
|
+
console.print(Panel(diff_text, title="[agent.accent]DIFF STAT[/agent.accent]", border_style="agent.border"))
|
|
42
|
+
else:
|
|
43
|
+
syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
|
|
44
|
+
console.print(Panel(syntax, title="[agent.accent]WORKING TREE DIFF[/agent.accent]", border_style="agent.border"))
|
|
45
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""CLI doctor command checking environment, toolchain, sandbox, and provider availability."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, SYM_WARN, console
|
|
13
|
+
from terminal_agent.cli.ui import render_header
|
|
14
|
+
from terminal_agent.config.schema import ProviderType
|
|
15
|
+
from terminal_agent.config.settings import load_config
|
|
16
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
17
|
+
from terminal_agent.providers.detector import OllamaStatus, ProviderStatus, detect_all_providers, detect_ollama
|
|
18
|
+
from terminal_agent.sandbox.docker import DockerSandbox
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def doctor_command(
|
|
22
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Check system toolchains, sandbox prerequisites, and model provider availability."""
|
|
25
|
+
render_header()
|
|
26
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
27
|
+
config = load_config(target_dir / "terminal-agent.config.yaml")
|
|
28
|
+
|
|
29
|
+
console.print("[agent.accent]Running system diagnostics and provider auto-detection...[/agent.accent]\n")
|
|
30
|
+
|
|
31
|
+
# 1. System & Toolchains Table
|
|
32
|
+
sys_table = Table(title="[bold agent.accent]SYSTEM & TOOLCHAINS[/bold agent.accent]", border_style="agent.border")
|
|
33
|
+
sys_table.add_column("Component", style="agent.text", width=22)
|
|
34
|
+
sys_table.add_column("Status", width=10)
|
|
35
|
+
sys_table.add_column("Details", style="agent.muted")
|
|
36
|
+
|
|
37
|
+
# Python Check
|
|
38
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
39
|
+
if sys.version_info >= (3, 10):
|
|
40
|
+
sys_table.add_row("Python Runtime", SYM_CHECK, f"Python {py_ver}")
|
|
41
|
+
else:
|
|
42
|
+
sys_table.add_row("Python Runtime", SYM_CROSS, f"Python {py_ver} (Requires 3.10+)")
|
|
43
|
+
|
|
44
|
+
# Git Check
|
|
45
|
+
git_path = shutil.which("git")
|
|
46
|
+
if git_path:
|
|
47
|
+
git_adapter = GitAdapter(target_dir)
|
|
48
|
+
is_repo = git_adapter.is_git_repo()
|
|
49
|
+
sys_table.add_row("Git CLI", SYM_CHECK, f"Installed, Repository Root: {'Yes' if is_repo else 'No'}")
|
|
50
|
+
else:
|
|
51
|
+
sys_table.add_row("Git CLI", SYM_CROSS, "Git is not installed or not in PATH")
|
|
52
|
+
|
|
53
|
+
# Local Sandbox Check
|
|
54
|
+
sys_table.add_row("Local Sandbox", SYM_CHECK, "Process-tree isolation with psutil timeout manager")
|
|
55
|
+
|
|
56
|
+
# Docker Sandbox Check
|
|
57
|
+
docker_ok = DockerSandbox.is_docker_available()
|
|
58
|
+
if docker_ok:
|
|
59
|
+
sys_table.add_row("Docker Sandbox", SYM_CHECK, "Docker daemon running & available")
|
|
60
|
+
else:
|
|
61
|
+
sys_table.add_row("Docker Sandbox", SYM_WARN, "Docker not running (Local sandbox used as default)")
|
|
62
|
+
|
|
63
|
+
# Workspace Permissions
|
|
64
|
+
try:
|
|
65
|
+
test_file = target_dir / ".doctor_perm_test.tmp"
|
|
66
|
+
test_file.write_text("ok", encoding="utf-8")
|
|
67
|
+
test_file.unlink()
|
|
68
|
+
sys_table.add_row("Workspace Access", SYM_CHECK, f"Read/Write OK on {target_dir.name}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
sys_table.add_row("Workspace Access", SYM_CROSS, f"Permission error: {e}")
|
|
71
|
+
|
|
72
|
+
console.print(sys_table)
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
# 2. Providers Detection Table
|
|
76
|
+
prov_table = Table(title="[bold agent.accent]MODEL PROVIDERS[/bold agent.accent]", border_style="agent.border")
|
|
77
|
+
prov_table.add_column("Provider", style="agent.text", width=22)
|
|
78
|
+
prov_table.add_column("Status", width=10)
|
|
79
|
+
prov_table.add_column("Details", style="agent.muted")
|
|
80
|
+
|
|
81
|
+
providers = detect_all_providers(config=config, working_dir=target_dir)
|
|
82
|
+
configured_provider_name = config.provider.name.value if hasattr(config.provider.name, "value") else str(config.provider.name)
|
|
83
|
+
|
|
84
|
+
any_usable = False
|
|
85
|
+
configured_is_usable = False
|
|
86
|
+
|
|
87
|
+
for prov_name, info in providers.items():
|
|
88
|
+
is_active_config = (prov_name == configured_provider_name)
|
|
89
|
+
display_label = f"{info.display_name} {'[bold cyan](Active)[/bold cyan]' if is_active_config else ''}"
|
|
90
|
+
|
|
91
|
+
if info.status == ProviderStatus.READY:
|
|
92
|
+
any_usable = True
|
|
93
|
+
if is_active_config:
|
|
94
|
+
configured_is_usable = True
|
|
95
|
+
prov_table.add_row(display_label, SYM_CHECK, info.details)
|
|
96
|
+
elif info.status == ProviderStatus.NOT_CONFIGURED:
|
|
97
|
+
# Optional missing provider -> SYM_WARN / Info, NOT SYM_CROSS
|
|
98
|
+
status_badge = SYM_WARN
|
|
99
|
+
hint_str = f" - Hint: {info.action_hint}" if info.action_hint else ""
|
|
100
|
+
prov_table.add_row(display_label, status_badge, f"{info.details}{hint_str}")
|
|
101
|
+
else:
|
|
102
|
+
# Error or explicitly selected but broken
|
|
103
|
+
status_badge = SYM_CROSS if is_active_config else SYM_WARN
|
|
104
|
+
hint_str = f" - Hint: {info.action_hint}" if info.action_hint else ""
|
|
105
|
+
prov_table.add_row(display_label, status_badge, f"{info.details}{hint_str}")
|
|
106
|
+
|
|
107
|
+
console.print(prov_table)
|
|
108
|
+
console.print()
|
|
109
|
+
|
|
110
|
+
# 3. Overall Readiness Assessment
|
|
111
|
+
if configured_is_usable or any_usable:
|
|
112
|
+
active_provider_info = providers.get(configured_provider_name)
|
|
113
|
+
if active_provider_info and active_provider_info.is_usable:
|
|
114
|
+
lines = [
|
|
115
|
+
"[bold agent.success]Overall Status: READY[/bold agent.success]",
|
|
116
|
+
f"Active Provider: [bold agent.accent]{active_provider_info.display_name}[/bold agent.accent] (Model: {config.provider.model})"
|
|
117
|
+
]
|
|
118
|
+
else:
|
|
119
|
+
usable_names = [p.display_name for p in providers.values() if p.is_usable and p.name != "mock"]
|
|
120
|
+
if not usable_names:
|
|
121
|
+
usable_names = ["Mock Provider"]
|
|
122
|
+
lines = [
|
|
123
|
+
"[bold agent.success]Overall Status: READY[/bold agent.success]",
|
|
124
|
+
f"Available Provider(s): [bold agent.accent]{', '.join(usable_names)}[/bold agent.accent]",
|
|
125
|
+
"[agent.muted]Run [bold]terminal-agent setup[/bold] to switch default provider.[/agent.muted]"
|
|
126
|
+
]
|
|
127
|
+
console.print(Panel("\n".join(lines), border_style="agent.success", title="[bold agent.success]DIAGNOSTIC RESULT[/bold agent.success]"))
|
|
128
|
+
else:
|
|
129
|
+
lines = [
|
|
130
|
+
"[bold agent.error]Overall Status: NOT READY[/bold agent.error]",
|
|
131
|
+
"[agent.warning]No model provider is currently configured or reachable.[/agent.warning]",
|
|
132
|
+
"",
|
|
133
|
+
"Next Action:",
|
|
134
|
+
" Run [bold agent.accent]terminal-agent setup[/bold agent.accent] to interactively configure Ollama or cloud API keys."
|
|
135
|
+
]
|
|
136
|
+
console.print(Panel("\n".join(lines), border_style="agent.error", title="[bold agent.error]DIAGNOSTIC RESULT[/bold agent.error]"))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""CLI resume command restoring and continuing an interrupted session."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from terminal_agent.agent.loop import AgentLoop
|
|
8
|
+
from terminal_agent.checkpoints.manager import CheckpointManager
|
|
9
|
+
from terminal_agent.cli.theme import SYM_CHECK, console
|
|
10
|
+
from terminal_agent.cli.ui import (
|
|
11
|
+
render_contract_card,
|
|
12
|
+
render_header,
|
|
13
|
+
render_plan,
|
|
14
|
+
render_proof_of_done,
|
|
15
|
+
render_task_card,
|
|
16
|
+
render_tool_event,
|
|
17
|
+
render_verification_result,
|
|
18
|
+
)
|
|
19
|
+
from terminal_agent.config.settings import load_config
|
|
20
|
+
from terminal_agent.context.engine import RepositoryContextEngine
|
|
21
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
22
|
+
from terminal_agent.providers.factory import create_provider
|
|
23
|
+
from terminal_agent.sandbox.local import LocalSandbox
|
|
24
|
+
from terminal_agent.security.policy import SecurityPolicyEnforcer
|
|
25
|
+
from terminal_agent.security.secrets import SecretGuard
|
|
26
|
+
from terminal_agent.session.manager import SessionManager
|
|
27
|
+
from terminal_agent.session.models import VerificationStatus
|
|
28
|
+
from terminal_agent.telemetry.events import TelemetryLogger
|
|
29
|
+
from terminal_agent.tools.registry import ToolRegistry
|
|
30
|
+
from terminal_agent.verifier.engine import IndependentVerifier
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resume_command(
|
|
34
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to resume (or latest if omitted)"),
|
|
35
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Resume an interrupted terminal agent session."""
|
|
38
|
+
render_header()
|
|
39
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
40
|
+
session_mgr = SessionManager(target_dir)
|
|
41
|
+
|
|
42
|
+
if session_id:
|
|
43
|
+
state = session_mgr.load_session(session_id)
|
|
44
|
+
else:
|
|
45
|
+
state = session_mgr.get_latest_session()
|
|
46
|
+
|
|
47
|
+
if not state:
|
|
48
|
+
console.print(f"[agent.error]No resumable session found in {target_dir}.[/agent.error]")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
console.print(f"\n[agent.accent]SESSION RESUMED: {state.session_id}[/agent.accent]")
|
|
52
|
+
render_task_card(state.task_description, target_dir)
|
|
53
|
+
|
|
54
|
+
if state.contract:
|
|
55
|
+
render_contract_card(state.contract)
|
|
56
|
+
if state.plan:
|
|
57
|
+
render_plan(state.plan)
|
|
58
|
+
|
|
59
|
+
console.print(f"[agent.muted]Completed steps: {len(state.completed_steps)}[/agent.muted]")
|
|
60
|
+
console.print(f"[agent.muted]Modified files: {', '.join(state.modified_files) if state.modified_files else 'None'}[/agent.muted]")
|
|
61
|
+
console.print(f"[agent.accent]Next: {state.next_step}[/agent.accent]\n")
|
|
62
|
+
|
|
63
|
+
if state.current_status == VerificationStatus.VERIFIED:
|
|
64
|
+
console.print("[agent.success]This session is already VERIFIED.[/agent.success]")
|
|
65
|
+
if state.proof_of_done:
|
|
66
|
+
render_proof_of_done(state.proof_of_done)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Set up runtime to continue execution
|
|
70
|
+
config = load_config(target_dir / "terminal-agent.config.yaml")
|
|
71
|
+
secret_guard = SecretGuard(blocked_patterns=config.security.blocked_paths, working_dir=target_dir)
|
|
72
|
+
security_enforcer = SecurityPolicyEnforcer(security_config=config.security, secret_guard=secret_guard)
|
|
73
|
+
sandbox = LocalSandbox(working_dir=target_dir, default_timeout=config.sandbox.timeout_seconds, secret_guard=secret_guard)
|
|
74
|
+
git_adapter = GitAdapter(target_dir)
|
|
75
|
+
checkpoint_mgr = CheckpointManager(target_dir)
|
|
76
|
+
context_engine = RepositoryContextEngine(target_dir, secret_guard=secret_guard)
|
|
77
|
+
tool_registry = ToolRegistry(target_dir, sandbox, security_enforcer, git_adapter, checkpoint_mgr, secret_guard)
|
|
78
|
+
verifier = IndependentVerifier(sandbox, git_adapter, config.verification, target_dir)
|
|
79
|
+
provider = create_provider(config.provider)
|
|
80
|
+
telemetry_logger = TelemetryLogger(state.session_id, working_dir=target_dir)
|
|
81
|
+
|
|
82
|
+
def on_event(event_type: str, data):
|
|
83
|
+
if event_type == "tool_completed":
|
|
84
|
+
render_tool_event(
|
|
85
|
+
tool_name=data.reason.split()[0] if data.reason else "tool",
|
|
86
|
+
reason=data.reason,
|
|
87
|
+
status="success" if data.success else "error",
|
|
88
|
+
duration_ms=data.duration_ms
|
|
89
|
+
)
|
|
90
|
+
elif event_type == "verification_result":
|
|
91
|
+
render_verification_result(data)
|
|
92
|
+
elif event_type == "proof_of_done":
|
|
93
|
+
render_proof_of_done(data, duration_seconds=state.metrics.execution_time_seconds)
|
|
94
|
+
|
|
95
|
+
agent_loop = AgentLoop(
|
|
96
|
+
session_state=state,
|
|
97
|
+
config=config,
|
|
98
|
+
provider=provider,
|
|
99
|
+
tool_registry=tool_registry,
|
|
100
|
+
verifier=verifier,
|
|
101
|
+
context_engine=context_engine,
|
|
102
|
+
checkpoint_manager=checkpoint_mgr,
|
|
103
|
+
session_manager=session_mgr,
|
|
104
|
+
telemetry_logger=telemetry_logger,
|
|
105
|
+
step_callback=on_event
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
agent_loop.run()
|
|
109
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""CLI run command executing autonomous coding tasks."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from terminal_agent.agent.loop import AgentLoop
|
|
9
|
+
from terminal_agent.checkpoints.manager import CheckpointManager
|
|
10
|
+
from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, console
|
|
11
|
+
from terminal_agent.cli.ui import (
|
|
12
|
+
render_contract_card,
|
|
13
|
+
render_header,
|
|
14
|
+
render_plan,
|
|
15
|
+
render_proof_of_done,
|
|
16
|
+
render_task_card,
|
|
17
|
+
render_tool_event,
|
|
18
|
+
render_verification_result,
|
|
19
|
+
)
|
|
20
|
+
from terminal_agent.config.settings import load_config
|
|
21
|
+
from terminal_agent.context.engine import RepositoryContextEngine
|
|
22
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
23
|
+
from terminal_agent.planner.contract import TaskContractGenerator
|
|
24
|
+
from terminal_agent.providers.detector import ProviderStatus, resolve_active_provider
|
|
25
|
+
from terminal_agent.providers.factory import create_provider
|
|
26
|
+
from terminal_agent.sandbox.docker import DockerSandbox
|
|
27
|
+
from terminal_agent.sandbox.local import LocalSandbox
|
|
28
|
+
from terminal_agent.security.policy import SecurityPolicyEnforcer
|
|
29
|
+
from terminal_agent.security.secrets import SecretGuard
|
|
30
|
+
from terminal_agent.session.manager import SessionManager
|
|
31
|
+
from terminal_agent.session.models import VerificationStatus
|
|
32
|
+
from terminal_agent.telemetry.events import TelemetryLogger
|
|
33
|
+
from terminal_agent.tools.registry import ToolRegistry
|
|
34
|
+
from terminal_agent.verifier.engine import IndependentVerifier
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_command(
|
|
38
|
+
task: Optional[str] = typer.Argument(None, help="The coding task description to execute autonomously"),
|
|
39
|
+
task_file: Optional[Path] = typer.Option(None, "--task", "-t", help="Path to declarative task YAML file"),
|
|
40
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
41
|
+
provider_name: Optional[str] = typer.Option(None, "--provider", "-p", help="Override model provider"),
|
|
42
|
+
model_name: Optional[str] = typer.Option(None, "--model", "-m", help="Override model name"),
|
|
43
|
+
auto_approve: bool = typer.Option(False, "--yes", "-y", help="Auto-approve confirmation prompts"),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Execute an autonomous verify-first coding session."""
|
|
46
|
+
render_header()
|
|
47
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
48
|
+
|
|
49
|
+
task_desc = task
|
|
50
|
+
if task_file and task_file.exists():
|
|
51
|
+
try:
|
|
52
|
+
content = yaml.safe_load(task_file.read_text(encoding="utf-8"))
|
|
53
|
+
if isinstance(content, dict):
|
|
54
|
+
task_desc = content.get("prompt") or content.get("task") or str(content)
|
|
55
|
+
else:
|
|
56
|
+
task_desc = str(content)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
console.print(f"[agent.error]Error reading task file: {e}[/agent.error]")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
if not task_desc:
|
|
62
|
+
task_desc = typer.prompt("Describe the coding task")
|
|
63
|
+
|
|
64
|
+
render_task_card(task_desc, target_dir)
|
|
65
|
+
|
|
66
|
+
# Load configuration & resolve active provider
|
|
67
|
+
config = load_config(target_dir / "terminal-agent.config.yaml")
|
|
68
|
+
resolved_prov_cfg, prov_info = resolve_active_provider(
|
|
69
|
+
config=config,
|
|
70
|
+
explicit_provider=provider_name,
|
|
71
|
+
explicit_model=model_name,
|
|
72
|
+
working_dir=target_dir
|
|
73
|
+
)
|
|
74
|
+
config.provider = resolved_prov_cfg
|
|
75
|
+
|
|
76
|
+
# Initialize subsystems
|
|
77
|
+
secret_guard = SecretGuard(blocked_patterns=config.security.blocked_paths, working_dir=target_dir)
|
|
78
|
+
|
|
79
|
+
def confirmation_cb(cmd: str, category, reason: str) -> bool:
|
|
80
|
+
if auto_approve:
|
|
81
|
+
return True
|
|
82
|
+
console.print(f"\n[agent.warning]Security Warning: {category.value.upper()} command[/agent.warning]")
|
|
83
|
+
console.print(f"Command: [agent.accent]{cmd}[/agent.accent]")
|
|
84
|
+
console.print(f"Reason: [agent.muted]{reason}[/agent.muted]")
|
|
85
|
+
return typer.confirm("Allow execution?")
|
|
86
|
+
|
|
87
|
+
security_enforcer = SecurityPolicyEnforcer(
|
|
88
|
+
security_config=config.security,
|
|
89
|
+
secret_guard=secret_guard,
|
|
90
|
+
confirmation_callback=confirmation_cb
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if config.sandbox.mode == "docker" and DockerSandbox.is_docker_available():
|
|
94
|
+
sandbox = DockerSandbox(
|
|
95
|
+
working_dir=target_dir,
|
|
96
|
+
image=config.sandbox.image,
|
|
97
|
+
network=config.sandbox.network,
|
|
98
|
+
timeout_seconds=config.sandbox.timeout_seconds,
|
|
99
|
+
secret_guard=secret_guard
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
sandbox = LocalSandbox(
|
|
103
|
+
working_dir=target_dir,
|
|
104
|
+
default_timeout=config.sandbox.timeout_seconds,
|
|
105
|
+
secret_guard=secret_guard
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
git_adapter = GitAdapter(target_dir)
|
|
109
|
+
checkpoint_mgr = CheckpointManager(target_dir)
|
|
110
|
+
session_mgr = SessionManager(target_dir)
|
|
111
|
+
context_engine = RepositoryContextEngine(target_dir, secret_guard=secret_guard)
|
|
112
|
+
|
|
113
|
+
tool_registry = ToolRegistry(
|
|
114
|
+
working_dir=target_dir,
|
|
115
|
+
sandbox=sandbox,
|
|
116
|
+
security_enforcer=security_enforcer,
|
|
117
|
+
git_adapter=git_adapter,
|
|
118
|
+
checkpoint_manager=checkpoint_mgr,
|
|
119
|
+
secret_guard=secret_guard
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
verifier = IndependentVerifier(
|
|
123
|
+
sandbox=sandbox,
|
|
124
|
+
git_adapter=git_adapter,
|
|
125
|
+
config=config.verification,
|
|
126
|
+
working_dir=target_dir
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Initialize Provider
|
|
130
|
+
provider = create_provider(config.provider)
|
|
131
|
+
health_ok, health_msg = provider.check_health()
|
|
132
|
+
if not health_ok and config.provider.name.value != "mock":
|
|
133
|
+
console.print(f"[agent.warning]Provider Notice ({prov_info.display_name}): {health_msg}[/agent.warning]")
|
|
134
|
+
if prov_info.action_hint:
|
|
135
|
+
console.print(f"[agent.accent]Suggested Action: {prov_info.action_hint}[/agent.accent]")
|
|
136
|
+
if not prov_info.is_usable:
|
|
137
|
+
console.print("\n[agent.muted]To configure or switch model providers, run: [bold]terminal-agent setup[/bold][/agent.muted]\n")
|
|
138
|
+
|
|
139
|
+
# Create Session
|
|
140
|
+
session_state = session_mgr.create_session(task_description=task_desc)
|
|
141
|
+
telemetry_logger = TelemetryLogger(session_state.session_id, working_dir=target_dir)
|
|
142
|
+
|
|
143
|
+
# Step callback for live display
|
|
144
|
+
def on_event(event_type: str, data: Any):
|
|
145
|
+
if event_type == "contract_ready":
|
|
146
|
+
render_contract_card(data)
|
|
147
|
+
if session_state.plan:
|
|
148
|
+
render_plan(session_state.plan)
|
|
149
|
+
elif event_type == "checkpoint_created":
|
|
150
|
+
console.print(f"{SYM_CHECK} [agent.muted]Created baseline checkpoint '{data.checkpoint_id}'[/agent.muted]")
|
|
151
|
+
elif event_type == "tool_completed":
|
|
152
|
+
render_tool_event(
|
|
153
|
+
tool_name=data.reason.split()[0] if data.reason else "tool",
|
|
154
|
+
reason=data.reason,
|
|
155
|
+
status="success" if data.success else "error",
|
|
156
|
+
duration_ms=data.duration_ms
|
|
157
|
+
)
|
|
158
|
+
elif event_type == "verification_result":
|
|
159
|
+
render_verification_result(data)
|
|
160
|
+
elif event_type == "repair_plan":
|
|
161
|
+
console.print(f"[agent.warning]Recovery Plan: Category '{data.failure_category.value}' -> {data.recovery_action}[/agent.warning]")
|
|
162
|
+
elif event_type == "proof_of_done":
|
|
163
|
+
render_proof_of_done(data, duration_seconds=session_state.metrics.execution_time_seconds)
|
|
164
|
+
|
|
165
|
+
agent_loop = AgentLoop(
|
|
166
|
+
session_state=session_state,
|
|
167
|
+
config=config,
|
|
168
|
+
provider=provider,
|
|
169
|
+
tool_registry=tool_registry,
|
|
170
|
+
verifier=verifier,
|
|
171
|
+
context_engine=context_engine,
|
|
172
|
+
checkpoint_manager=checkpoint_mgr,
|
|
173
|
+
session_manager=session_mgr,
|
|
174
|
+
telemetry_logger=telemetry_logger,
|
|
175
|
+
step_callback=on_event
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Run Loop
|
|
179
|
+
final_state = agent_loop.run()
|
|
180
|
+
|
|
181
|
+
if final_state.current_status == VerificationStatus.VERIFIED:
|
|
182
|
+
console.print(f"\n[agent.success]Task successfully VERIFIED. Session: {final_state.session_id}[/agent.success]")
|
|
183
|
+
else:
|
|
184
|
+
console.print(f"\n[agent.error]Task concluded with status: {final_state.current_status.value}[/agent.error]")
|