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,229 @@
|
|
|
1
|
+
"""Interactive CLI setup command for configuring model providers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.prompt import Confirm, Prompt
|
|
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 ProviderConfig, ProviderType, TerminalAgentConfig
|
|
15
|
+
from terminal_agent.config.settings import load_config
|
|
16
|
+
from terminal_agent.providers.detector import OllamaStatus, detect_ollama, detect_all_providers
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_command(
|
|
21
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Interactively configure the model provider (Ollama, OpenAI, Anthropic, Gemini)."""
|
|
24
|
+
render_header()
|
|
25
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
26
|
+
config_file = target_dir / "terminal-agent.config.yaml"
|
|
27
|
+
config = load_config(config_file)
|
|
28
|
+
|
|
29
|
+
panel_text = (
|
|
30
|
+
"[bold agent.accent]Terminal Agent Model Provider Setup[/bold agent.accent]\n"
|
|
31
|
+
"[agent.muted]Configure your local or cloud LLM provider to start using Terminal Agent.[/agent.muted]"
|
|
32
|
+
)
|
|
33
|
+
console.print(Panel(panel_text, border_style="agent.border"))
|
|
34
|
+
|
|
35
|
+
console.print("\n[agent.accent]Select a model provider:[/agent.accent]")
|
|
36
|
+
console.print(" [bold]1.[/bold] Ollama (Local open-source models, Free, Private)")
|
|
37
|
+
console.print(" [bold]2.[/bold] OpenAI (GPT-4o, GPT-4o-mini)")
|
|
38
|
+
console.print(" [bold]3.[/bold] Anthropic Claude (Claude 3.5 Sonnet)")
|
|
39
|
+
console.print(" [bold]4.[/bold] Google Gemini (Gemini 1.5 Pro/Flash)")
|
|
40
|
+
console.print(" [bold]5.[/bold] Mock Provider (Deterministic Testing/Offline)")
|
|
41
|
+
|
|
42
|
+
choice = Prompt.ask("\nChoose option", choices=["1", "2", "3", "4", "5"], default="1")
|
|
43
|
+
|
|
44
|
+
if choice == "1":
|
|
45
|
+
_setup_ollama(config, config_file, target_dir)
|
|
46
|
+
elif choice == "2":
|
|
47
|
+
_setup_cloud_provider(
|
|
48
|
+
provider_type=ProviderType.OPENAI,
|
|
49
|
+
provider_name="OpenAI",
|
|
50
|
+
env_var_name="OPENAI_API_KEY",
|
|
51
|
+
default_model="gpt-4o",
|
|
52
|
+
config=config,
|
|
53
|
+
config_file=config_file,
|
|
54
|
+
target_dir=target_dir
|
|
55
|
+
)
|
|
56
|
+
elif choice == "3":
|
|
57
|
+
_setup_cloud_provider(
|
|
58
|
+
provider_type=ProviderType.ANTHROPIC,
|
|
59
|
+
provider_name="Anthropic",
|
|
60
|
+
env_var_name="ANTHROPIC_API_KEY",
|
|
61
|
+
default_model="claude-3-5-sonnet-20241022",
|
|
62
|
+
config=config,
|
|
63
|
+
config_file=config_file,
|
|
64
|
+
target_dir=target_dir
|
|
65
|
+
)
|
|
66
|
+
elif choice == "4":
|
|
67
|
+
_setup_cloud_provider(
|
|
68
|
+
provider_type=ProviderType.GEMINI,
|
|
69
|
+
provider_name="Google Gemini",
|
|
70
|
+
env_var_name="GEMINI_API_KEY",
|
|
71
|
+
default_model="gemini-1.5-pro",
|
|
72
|
+
config=config,
|
|
73
|
+
config_file=config_file,
|
|
74
|
+
target_dir=target_dir
|
|
75
|
+
)
|
|
76
|
+
elif choice == "5":
|
|
77
|
+
_save_provider_config(config, config_file, ProviderType.MOCK, "mock-model")
|
|
78
|
+
console.print(f"\n{SYM_CHECK} [agent.success]Configured Mock Provider for deterministic offline testing.[/agent.success]")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _setup_ollama(config: TerminalAgentConfig, config_file: Path, target_dir: Path) -> None:
|
|
82
|
+
"""Setup and diagnostic flow for Ollama local provider."""
|
|
83
|
+
console.print("\n[agent.accent]Checking local Ollama installation...[/agent.accent]")
|
|
84
|
+
target_model = Prompt.ask("Enter target Ollama model name", default="qwen2.5-coder")
|
|
85
|
+
|
|
86
|
+
ollama_res = detect_ollama(target_model=target_model)
|
|
87
|
+
|
|
88
|
+
if ollama_res.status == OllamaStatus.OLLAMA_NOT_INSTALLED:
|
|
89
|
+
console.print(f"\n{SYM_WARN} [agent.warning]Ollama executable was not found in your PATH.[/agent.warning]")
|
|
90
|
+
console.print("[agent.muted]To install Ollama, visit:[/agent.muted] [bold cyan]https://ollama.com/download[/bold cyan]")
|
|
91
|
+
console.print(f"\nAfter installing, start Ollama and run: [agent.accent]ollama pull {target_model}[/agent.accent]")
|
|
92
|
+
|
|
93
|
+
save_anyway = Confirm.ask("Do you want to save Ollama as the configured provider anyway?", default=True)
|
|
94
|
+
if save_anyway:
|
|
95
|
+
_save_provider_config(config, config_file, ProviderType.OLLAMA, target_model)
|
|
96
|
+
console.print(f"{SYM_CHECK} [agent.success]Saved Ollama configuration.[/agent.success]")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
elif ollama_res.status == OllamaStatus.OLLAMA_NOT_RUNNING:
|
|
100
|
+
console.print(f"\n{SYM_WARN} [agent.warning]Ollama is installed ({ollama_res.cli_path}), but the Ollama server is not running.[/agent.warning]")
|
|
101
|
+
console.print("[agent.muted]Please start the Ollama application or run in a terminal:[/agent.muted]")
|
|
102
|
+
console.print(" [bold agent.accent]ollama serve[/bold agent.accent]\n")
|
|
103
|
+
|
|
104
|
+
_save_provider_config(config, config_file, ProviderType.OLLAMA, target_model)
|
|
105
|
+
console.print(f"{SYM_CHECK} [agent.success]Saved Ollama configuration with model '{target_model}'.[/agent.success]")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
elif ollama_res.status == OllamaStatus.OLLAMA_MODEL_MISSING:
|
|
109
|
+
console.print(f"\n{SYM_WARN} [agent.warning]Ollama server is online, but model '{target_model}' is not installed locally.[/agent.warning]")
|
|
110
|
+
if ollama_res.installed_models:
|
|
111
|
+
console.print(f"[agent.muted]Currently installed models:[/agent.muted] {', '.join(ollama_res.installed_models)}")
|
|
112
|
+
|
|
113
|
+
should_pull = Confirm.ask(f"Would you like to run 'ollama pull {target_model}' now?", default=False)
|
|
114
|
+
if should_pull:
|
|
115
|
+
console.print(f"\n[agent.accent]Pulling model '{target_model}' via Ollama CLI...[/agent.accent]")
|
|
116
|
+
try:
|
|
117
|
+
subprocess.run(["ollama", "pull", target_model], check=True)
|
|
118
|
+
console.print(f"{SYM_CHECK} [agent.success]Successfully pulled model '{target_model}'![/agent.success]")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
console.print(f"{SYM_CROSS} [agent.error]Failed to pull model: {e}[/agent.error]")
|
|
121
|
+
console.print(f"You can pull it manually later with: [bold]ollama pull {target_model}[/bold]")
|
|
122
|
+
else:
|
|
123
|
+
console.print(f"[agent.muted]You can pull it manually with: [bold]ollama pull {target_model}[/bold][/agent.muted]")
|
|
124
|
+
|
|
125
|
+
_save_provider_config(config, config_file, ProviderType.OLLAMA, target_model)
|
|
126
|
+
console.print(f"\n{SYM_CHECK} [agent.success]Configured Ollama with model '{target_model}'.[/agent.success]")
|
|
127
|
+
|
|
128
|
+
elif ollama_res.status == OllamaStatus.OLLAMA_READY:
|
|
129
|
+
console.print(f"\n{SYM_CHECK} [agent.success]Ollama server is online and model '{target_model}' is ready to use.[/agent.success]")
|
|
130
|
+
_save_provider_config(config, config_file, ProviderType.OLLAMA, target_model)
|
|
131
|
+
console.print(f"{SYM_CHECK} [agent.success]Configured Ollama with model '{target_model}'.[/agent.success]")
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
console.print(f"\n{SYM_CROSS} [agent.error]Ollama check returned error: {ollama_res.message}[/agent.error]")
|
|
135
|
+
_save_provider_config(config, config_file, ProviderType.OLLAMA, target_model)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _setup_cloud_provider(
|
|
139
|
+
provider_type: ProviderType,
|
|
140
|
+
provider_name: str,
|
|
141
|
+
env_var_name: str,
|
|
142
|
+
default_model: str,
|
|
143
|
+
config: TerminalAgentConfig,
|
|
144
|
+
config_file: Path,
|
|
145
|
+
target_dir: Path
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Setup flow for cloud API providers (OpenAI, Anthropic, Gemini)."""
|
|
148
|
+
console.print(f"\n[agent.accent]Configuring {provider_name}...[/agent.accent]")
|
|
149
|
+
|
|
150
|
+
# Check existing environment variable
|
|
151
|
+
existing_key = os.environ.get(env_var_name)
|
|
152
|
+
model = Prompt.ask("Model identifier", default=default_model)
|
|
153
|
+
|
|
154
|
+
if existing_key and len(existing_key.strip()) > 5:
|
|
155
|
+
masked = existing_key[:4] + "..." + existing_key[-4:] if len(existing_key) > 8 else "****"
|
|
156
|
+
console.print(f"{SYM_CHECK} Found {env_var_name} in environment: [bold agent.muted]{masked}[/bold agent.muted]")
|
|
157
|
+
else:
|
|
158
|
+
console.print(f"[agent.muted]No {env_var_name} detected in your current shell environment.[/agent.muted]")
|
|
159
|
+
api_key = typer.prompt(f"Enter your {provider_name} API Key (input will be hidden)", hide_input=True)
|
|
160
|
+
|
|
161
|
+
if api_key and len(api_key.strip()) > 5:
|
|
162
|
+
save_to_env = Confirm.ask(f"Save this key to local '.env' file in {target_dir.name}? (Excluded from git)", default=True)
|
|
163
|
+
if save_to_env:
|
|
164
|
+
env_file = target_dir / ".env"
|
|
165
|
+
_append_or_update_env_file(env_file, env_var_name, api_key.strip())
|
|
166
|
+
console.print(f"{SYM_CHECK} [agent.success]Saved {env_var_name} to {env_file.name}.[/agent.success]")
|
|
167
|
+
else:
|
|
168
|
+
console.print("[agent.muted]To set this manually in your shell, run:[/agent.muted]")
|
|
169
|
+
console.print(f" [bold]export {env_var_name}='your-api-key'[/bold] (Linux/macOS)")
|
|
170
|
+
console.print(f" [bold]$env:{env_var_name}='your-api-key'[/bold] (Windows PowerShell)")
|
|
171
|
+
|
|
172
|
+
_save_provider_config(config, config_file, provider_type, model)
|
|
173
|
+
console.print(f"\n{SYM_CHECK} [agent.success]Successfully configured {provider_name} (Model: {model}).[/agent.success]")
|
|
174
|
+
console.print("[agent.muted]Next step: Run [bold]terminal-agent doctor[/bold] or start a task with [bold]terminal-agent run \"...\"[/bold][/agent.muted]")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _append_or_update_env_file(env_file: Path, key: str, value: str) -> None:
|
|
178
|
+
"""Helper to safely write or update key-value in a local .env file."""
|
|
179
|
+
lines: list[str] = []
|
|
180
|
+
found = False
|
|
181
|
+
if env_file.exists():
|
|
182
|
+
lines = env_file.read_text(encoding="utf-8").splitlines()
|
|
183
|
+
for i, line in enumerate(lines):
|
|
184
|
+
if line.strip().startswith(f"{key}=") or line.strip().startswith(f"export {key}="):
|
|
185
|
+
lines[i] = f"{key}={value}"
|
|
186
|
+
found = True
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if not found:
|
|
190
|
+
lines.append(f"{key}={value}")
|
|
191
|
+
|
|
192
|
+
env_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _save_provider_config(
|
|
196
|
+
config: TerminalAgentConfig,
|
|
197
|
+
config_file: Path,
|
|
198
|
+
provider_type: ProviderType,
|
|
199
|
+
model: str
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Persist non-secret provider preferences into terminal-agent.config.yaml."""
|
|
202
|
+
config.provider.name = provider_type
|
|
203
|
+
config.provider.model = model
|
|
204
|
+
|
|
205
|
+
raw_data = {}
|
|
206
|
+
if config_file.exists():
|
|
207
|
+
try:
|
|
208
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
209
|
+
loaded = yaml.safe_load(f)
|
|
210
|
+
if isinstance(loaded, dict):
|
|
211
|
+
raw_data = loaded
|
|
212
|
+
except Exception:
|
|
213
|
+
raw_data = {}
|
|
214
|
+
|
|
215
|
+
if "provider" not in raw_data:
|
|
216
|
+
raw_data["provider"] = {}
|
|
217
|
+
|
|
218
|
+
raw_data["provider"]["name"] = provider_type.value
|
|
219
|
+
raw_data["provider"]["model"] = model
|
|
220
|
+
|
|
221
|
+
if not raw_data or len(raw_data) <= 1:
|
|
222
|
+
raw_data = config.model_dump(mode="json")
|
|
223
|
+
raw_data["provider"]["name"] = provider_type.value
|
|
224
|
+
raw_data["provider"]["model"] = model
|
|
225
|
+
if "api_key" in raw_data["provider"] and raw_data["provider"]["api_key"]:
|
|
226
|
+
raw_data["provider"]["api_key"] = None
|
|
227
|
+
|
|
228
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
229
|
+
yaml.dump(raw_data, f, sort_keys=False, indent=2)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI status command inspecting active repository and latest session state."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, console
|
|
10
|
+
from terminal_agent.cli.ui import render_header, render_plan, render_task_card
|
|
11
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
12
|
+
from terminal_agent.session.manager import SessionManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def status_command(
|
|
16
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Inspect repository status and latest agent session."""
|
|
19
|
+
render_header()
|
|
20
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
21
|
+
|
|
22
|
+
# Git Status
|
|
23
|
+
git_adapter = GitAdapter(target_dir)
|
|
24
|
+
status = git_adapter.get_status()
|
|
25
|
+
diff_stats = git_adapter.get_diff_stats()
|
|
26
|
+
|
|
27
|
+
t_git = Table(box=None, show_header=False)
|
|
28
|
+
t_git.add_column(style="agent.muted", width=16)
|
|
29
|
+
t_git.add_column(style="agent.text")
|
|
30
|
+
|
|
31
|
+
t_git.add_row("GIT REPO", "[agent.success]Yes[/agent.success]" if status.is_repo else "[agent.muted]No[/agent.muted]")
|
|
32
|
+
if status.is_repo:
|
|
33
|
+
t_git.add_row("BRANCH", status.branch)
|
|
34
|
+
t_git.add_row("WORKING TREE", "[agent.success]Clean[/agent.success]" if status.is_clean else "[agent.warning]Modified[/agent.warning]")
|
|
35
|
+
t_git.add_row("MODIFIED FILES", f"{len(status.modified_files)} ({', '.join(status.modified_files) if status.modified_files else 'None'})")
|
|
36
|
+
t_git.add_row("UNTRACKED FILES", f"{len(status.untracked_files)} ({', '.join(status.untracked_files) if status.untracked_files else 'None'})")
|
|
37
|
+
t_git.add_row("DIFF STATS", f"+{diff_stats.insertions} / -{diff_stats.deletions} lines across {diff_stats.files_changed} files")
|
|
38
|
+
|
|
39
|
+
console.print(Panel(t_git, title="[agent.accent]REPOSITORY STATUS[/agent.accent]", border_style="agent.border"))
|
|
40
|
+
|
|
41
|
+
# Session Status
|
|
42
|
+
session_mgr = SessionManager(target_dir)
|
|
43
|
+
latest = session_mgr.get_latest_session()
|
|
44
|
+
|
|
45
|
+
if latest:
|
|
46
|
+
t_sess = Table(box=None, show_header=False)
|
|
47
|
+
t_sess.add_column(style="agent.muted", width=16)
|
|
48
|
+
t_sess.add_column(style="agent.text")
|
|
49
|
+
|
|
50
|
+
t_sess.add_row("SESSION ID", latest.session_id)
|
|
51
|
+
t_sess.add_row("STATUS", f"[agent.accent]{latest.current_status.value}[/agent.accent]")
|
|
52
|
+
t_sess.add_row("TASK", latest.task_description)
|
|
53
|
+
t_sess.add_row("STEPS COMPLETED", str(len(latest.completed_steps)))
|
|
54
|
+
t_sess.add_row("RETRIES", str(len(latest.failures)))
|
|
55
|
+
t_sess.add_row("LAST UPDATED", latest.updated_at)
|
|
56
|
+
|
|
57
|
+
console.print(Panel(t_sess, title="[agent.accent]LATEST AGENT SESSION[/agent.accent]", border_style="agent.border"))
|
|
58
|
+
if latest.plan:
|
|
59
|
+
render_plan(latest.plan)
|
|
60
|
+
else:
|
|
61
|
+
console.print("[agent.muted]No previous agent sessions recorded in this workspace.[/agent.muted]")
|
|
62
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""CLI test command running independent verification suite."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from terminal_agent.cli.theme import console
|
|
8
|
+
from terminal_agent.cli.ui import render_header, render_verification_result
|
|
9
|
+
from terminal_agent.config.settings import load_config
|
|
10
|
+
from terminal_agent.git.adapter import GitAdapter
|
|
11
|
+
from terminal_agent.sandbox.local import LocalSandbox
|
|
12
|
+
from terminal_agent.security.secrets import SecretGuard
|
|
13
|
+
from terminal_agent.session.models import VerificationStatus
|
|
14
|
+
from terminal_agent.verifier.engine import IndependentVerifier
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_command(
|
|
18
|
+
test_cmd: Optional[str] = typer.Option(None, "--cmd", "-c", help="Custom test command override"),
|
|
19
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Run independent verification test suite on current workspace."""
|
|
22
|
+
render_header()
|
|
23
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
24
|
+
|
|
25
|
+
config = load_config(target_dir / "terminal-agent.config.yaml")
|
|
26
|
+
secret_guard = SecretGuard(blocked_patterns=config.security.blocked_paths, working_dir=target_dir)
|
|
27
|
+
sandbox = LocalSandbox(working_dir=target_dir, default_timeout=config.sandbox.timeout_seconds, secret_guard=secret_guard)
|
|
28
|
+
git_adapter = GitAdapter(target_dir)
|
|
29
|
+
|
|
30
|
+
verifier = IndependentVerifier(sandbox, git_adapter, config.verification, target_dir)
|
|
31
|
+
|
|
32
|
+
console.print(f"[agent.muted]Running independent verification in {target_dir}...[/agent.muted]\n")
|
|
33
|
+
result = verifier.verify(custom_test_cmd=test_cmd)
|
|
34
|
+
render_verification_result(result)
|
|
35
|
+
|
|
36
|
+
if result.status != VerificationStatus.VERIFIED:
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""CLI trace command inspecting telemetry event streams."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, console
|
|
9
|
+
from terminal_agent.cli.ui import render_header
|
|
10
|
+
from terminal_agent.session.manager import SessionManager
|
|
11
|
+
from terminal_agent.telemetry.events import TelemetryLogger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def trace_command(
|
|
15
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to inspect (defaults to latest)"),
|
|
16
|
+
working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Inspect structured telemetry event trace of an agent session."""
|
|
19
|
+
render_header()
|
|
20
|
+
target_dir = (working_dir or Path.cwd()).resolve()
|
|
21
|
+
|
|
22
|
+
if not session_id:
|
|
23
|
+
session_mgr = SessionManager(target_dir)
|
|
24
|
+
latest = session_mgr.get_latest_session()
|
|
25
|
+
if latest:
|
|
26
|
+
session_id = latest.session_id
|
|
27
|
+
else:
|
|
28
|
+
console.print("[agent.error]No active or past sessions found.[/agent.error]")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
|
|
31
|
+
events = TelemetryLogger.read_trace(session_id, working_dir=target_dir)
|
|
32
|
+
|
|
33
|
+
if not events:
|
|
34
|
+
console.print(f"[agent.muted]No trace events recorded for session '{session_id}'.[/agent.muted]")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
table = Table(title=f"[agent.accent]TELEMETRY TRACE: {session_id}[/agent.accent]", border_style="agent.border")
|
|
38
|
+
table.add_column("Time", style="agent.muted")
|
|
39
|
+
table.add_column("Step", style="agent.accent", width=6)
|
|
40
|
+
table.add_column("Event", style="agent.text", width=14)
|
|
41
|
+
table.add_column("Tool", style="agent.accent", width=16)
|
|
42
|
+
table.add_column("Status", width=8)
|
|
43
|
+
table.add_column("Duration", style="agent.muted", width=10)
|
|
44
|
+
table.add_column("Reason / Context", style="agent.text")
|
|
45
|
+
|
|
46
|
+
for ev in events:
|
|
47
|
+
time_str = ev.timestamp[11:19]
|
|
48
|
+
status_sym = SYM_CHECK if ev.status == "success" else SYM_CROSS
|
|
49
|
+
table.add_row(
|
|
50
|
+
time_str,
|
|
51
|
+
str(ev.step),
|
|
52
|
+
ev.event_type,
|
|
53
|
+
ev.tool or "-",
|
|
54
|
+
status_sym,
|
|
55
|
+
f"{ev.duration_ms}ms",
|
|
56
|
+
ev.reason or "-"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
console.print(table)
|
|
60
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Main Typer CLI entrypoint for Terminal Agent."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from terminal_agent.cli.commands.checkpoint import checkpoint_app, rollback_command
|
|
10
|
+
from terminal_agent.cli.commands.config_cmd import config_command
|
|
11
|
+
from terminal_agent.cli.commands.diff import diff_command
|
|
12
|
+
from terminal_agent.cli.commands.doctor import doctor_command
|
|
13
|
+
from terminal_agent.cli.commands.resume import resume_command
|
|
14
|
+
from terminal_agent.cli.commands.run import run_command
|
|
15
|
+
from terminal_agent.cli.commands.setup import setup_command
|
|
16
|
+
from terminal_agent.cli.commands.status import status_command
|
|
17
|
+
from terminal_agent.cli.commands.test import test_command
|
|
18
|
+
from terminal_agent.cli.commands.clean import clean_command
|
|
19
|
+
from terminal_agent.cli.commands.trace import trace_command
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool):
|
|
23
|
+
if value:
|
|
24
|
+
try:
|
|
25
|
+
ver = importlib.metadata.version("terminal-agent-cli")
|
|
26
|
+
except Exception:
|
|
27
|
+
try:
|
|
28
|
+
ver = importlib.metadata.version("terminal-agent")
|
|
29
|
+
except Exception:
|
|
30
|
+
from terminal_agent import __version__ as ver
|
|
31
|
+
|
|
32
|
+
typer.echo(f"terminal-agent v{ver}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
name="terminal-agent",
|
|
38
|
+
help="Terminal Agent: Build. Verify. Ship. Autonomous terminal-based coding agent.",
|
|
39
|
+
no_args_is_help=False
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def main(
|
|
45
|
+
version: Optional[bool] = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--version",
|
|
48
|
+
"-v",
|
|
49
|
+
callback=version_callback,
|
|
50
|
+
is_eager=True,
|
|
51
|
+
help="Show the version and exit.",
|
|
52
|
+
)
|
|
53
|
+
):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Register Subcommands
|
|
58
|
+
app.command(name="run", help="Run an autonomous coding task.")(run_command)
|
|
59
|
+
app.command(name="setup", help="Interactively configure model providers (Ollama, OpenAI, Claude, Gemini).")(setup_command)
|
|
60
|
+
app.command(name="resume", help="Resume an interrupted agent session.")(resume_command)
|
|
61
|
+
app.command(name="status", help="Inspect workspace git status and active session.")(status_command)
|
|
62
|
+
app.command(name="diff", help="View working tree git diff and stats.")(diff_command)
|
|
63
|
+
app.command(name="test", help="Execute independent verification test suite.")(test_command)
|
|
64
|
+
app.add_typer(checkpoint_app, name="checkpoint")
|
|
65
|
+
app.command(name="rollback", help="Restore repository state to a previous checkpoint.")(rollback_command)
|
|
66
|
+
app.command(name="doctor", help="Check system health, sandboxing, and provider status.")(doctor_command)
|
|
67
|
+
app.command(name="trace", help="View structured telemetry trace for a session.")(trace_command)
|
|
68
|
+
app.command(name="config", help="View or initialize configuration file.")(config_command)
|
|
69
|
+
app.command(name="clean", help="Prune old sessions, checkpoints, and telemetry files.")(clean_command)
|
|
70
|
+
|
|
71
|
+
KNOWN_COMMANDS = {
|
|
72
|
+
"run", "setup", "resume", "status", "diff", "test", "checkpoint",
|
|
73
|
+
"rollback", "doctor", "trace", "config", "clean", "--help", "-h",
|
|
74
|
+
"--version", "-v"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cli():
|
|
79
|
+
"""Main CLI entrypoint handling automatic default 'run' routing."""
|
|
80
|
+
args = sys.argv[1:]
|
|
81
|
+
if args:
|
|
82
|
+
first_arg = args[0]
|
|
83
|
+
if first_arg not in KNOWN_COMMANDS and not first_arg.startswith("-"):
|
|
84
|
+
# Route bare string task to 'run' subcommand
|
|
85
|
+
sys.argv.insert(1, "run")
|
|
86
|
+
elif len(sys.argv) == 1:
|
|
87
|
+
# Bare terminal-agent invocation -> prompt in run command
|
|
88
|
+
sys.argv.append("run")
|
|
89
|
+
|
|
90
|
+
app()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
cli()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Rich theme and color definitions adhering to Terminal Agent monochrome aesthetic."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.theme import Theme
|
|
7
|
+
|
|
8
|
+
# Force UTF-8 on Windows terminal if available
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
try:
|
|
11
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
# Terminal Agent Monochrome Palette
|
|
19
|
+
PALETTE = {
|
|
20
|
+
"bg": "#0A0A0A",
|
|
21
|
+
"text": "#EDEDED",
|
|
22
|
+
"muted": "#8A8A8A",
|
|
23
|
+
"success": "#69F0AE",
|
|
24
|
+
"warning": "#FFC857",
|
|
25
|
+
"error": "#FF5F56",
|
|
26
|
+
"accent": "#F5F5F5",
|
|
27
|
+
"cyan_accent": "#00E5FF",
|
|
28
|
+
"orange_accent": "#FF9100",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TERMINAL_AGENT_THEME = Theme({
|
|
32
|
+
"agent.title": f"bold {PALETTE['accent']}",
|
|
33
|
+
"agent.tagline": f"italic {PALETTE['muted']}",
|
|
34
|
+
"agent.text": PALETTE["text"],
|
|
35
|
+
"agent.muted": PALETTE["muted"],
|
|
36
|
+
"agent.success": f"bold {PALETTE['success']}",
|
|
37
|
+
"agent.warning": f"bold {PALETTE['warning']}",
|
|
38
|
+
"agent.error": f"bold {PALETTE['error']}",
|
|
39
|
+
"agent.accent": f"bold {PALETTE['accent']}",
|
|
40
|
+
"agent.cyan": PALETTE["cyan_accent"],
|
|
41
|
+
"agent.orange": PALETTE["orange_accent"],
|
|
42
|
+
"agent.code": f"{PALETTE['accent']} on #1E1E1E",
|
|
43
|
+
"agent.border": "#333333",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
console = Console(theme=TERMINAL_AGENT_THEME, legacy_windows=False)
|
|
47
|
+
error_console = Console(theme=TERMINAL_AGENT_THEME, stderr=True, legacy_windows=False)
|
|
48
|
+
|
|
49
|
+
# Symbols (Safe UTF-8 with fallback)
|
|
50
|
+
SYM_CHECK = "[agent.success]✓[/agent.success]"
|
|
51
|
+
SYM_CROSS = "[agent.error]✗[/agent.error]"
|
|
52
|
+
SYM_ARROW = "[agent.accent]→[/agent.accent]"
|
|
53
|
+
SYM_BULLET = "[agent.muted]•[/agent.muted]"
|
|
54
|
+
SYM_WARN = "[agent.warning]![/agent.warning]"
|
|
55
|
+
|