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.
Files changed (74) hide show
  1. terminal_agent/__init__.py +7 -0
  2. terminal_agent/__main__.py +7 -0
  3. terminal_agent/agent/__init__.py +9 -0
  4. terminal_agent/agent/loop.py +338 -0
  5. terminal_agent/checkpoints/__init__.py +9 -0
  6. terminal_agent/checkpoints/manager.py +151 -0
  7. terminal_agent/cli/commands/__init__.py +26 -0
  8. terminal_agent/cli/commands/checkpoint.py +76 -0
  9. terminal_agent/cli/commands/clean.py +170 -0
  10. terminal_agent/cli/commands/config_cmd.py +32 -0
  11. terminal_agent/cli/commands/diff.py +45 -0
  12. terminal_agent/cli/commands/doctor.py +136 -0
  13. terminal_agent/cli/commands/resume.py +109 -0
  14. terminal_agent/cli/commands/run.py +184 -0
  15. terminal_agent/cli/commands/setup.py +229 -0
  16. terminal_agent/cli/commands/status.py +62 -0
  17. terminal_agent/cli/commands/test.py +38 -0
  18. terminal_agent/cli/commands/trace.py +60 -0
  19. terminal_agent/cli/main.py +94 -0
  20. terminal_agent/cli/theme.py +55 -0
  21. terminal_agent/cli/ui.py +147 -0
  22. terminal_agent/config/__init__.py +30 -0
  23. terminal_agent/config/schema.py +100 -0
  24. terminal_agent/config/settings.py +85 -0
  25. terminal_agent/context/__init__.py +11 -0
  26. terminal_agent/context/engine.py +173 -0
  27. terminal_agent/context/ranker.py +87 -0
  28. terminal_agent/git/__init__.py +10 -0
  29. terminal_agent/git/adapter.py +219 -0
  30. terminal_agent/planner/__init__.py +10 -0
  31. terminal_agent/planner/contract.py +58 -0
  32. terminal_agent/planner/planner.py +30 -0
  33. terminal_agent/providers/__init__.py +30 -0
  34. terminal_agent/providers/anthropic_provider.py +141 -0
  35. terminal_agent/providers/base.py +57 -0
  36. terminal_agent/providers/detector.py +397 -0
  37. terminal_agent/providers/factory.py +29 -0
  38. terminal_agent/providers/gemini_provider.py +130 -0
  39. terminal_agent/providers/mock_provider.py +80 -0
  40. terminal_agent/providers/ollama_provider.py +124 -0
  41. terminal_agent/providers/openai_provider.py +124 -0
  42. terminal_agent/recovery/__init__.py +10 -0
  43. terminal_agent/recovery/classifier.py +64 -0
  44. terminal_agent/recovery/strategies.py +62 -0
  45. terminal_agent/sandbox/__init__.py +13 -0
  46. terminal_agent/sandbox/base.py +41 -0
  47. terminal_agent/sandbox/docker.py +129 -0
  48. terminal_agent/sandbox/local.py +129 -0
  49. terminal_agent/security/__init__.py +13 -0
  50. terminal_agent/security/classifier.py +151 -0
  51. terminal_agent/security/policy.py +51 -0
  52. terminal_agent/security/secrets.py +105 -0
  53. terminal_agent/session/__init__.py +33 -0
  54. terminal_agent/session/manager.py +195 -0
  55. terminal_agent/session/models.py +160 -0
  56. terminal_agent/telemetry/__init__.py +12 -0
  57. terminal_agent/telemetry/events.py +87 -0
  58. terminal_agent/telemetry/metrics.py +38 -0
  59. terminal_agent/tools/__init__.py +33 -0
  60. terminal_agent/tools/base.py +59 -0
  61. terminal_agent/tools/checkpoint_tools.py +72 -0
  62. terminal_agent/tools/command_tools.py +125 -0
  63. terminal_agent/tools/file_tools.py +340 -0
  64. terminal_agent/tools/git_tools.py +124 -0
  65. terminal_agent/tools/registry.py +109 -0
  66. terminal_agent/verifier/__init__.py +13 -0
  67. terminal_agent/verifier/assertions.py +115 -0
  68. terminal_agent/verifier/engine.py +125 -0
  69. terminal_agent/verifier/runners.py +80 -0
  70. terminal_agent_cli-0.1.0.dist-info/METADATA +438 -0
  71. terminal_agent_cli-0.1.0.dist-info/RECORD +74 -0
  72. terminal_agent_cli-0.1.0.dist-info/WHEEL +4 -0
  73. terminal_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  74. 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
+