agent-scout-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.
@@ -0,0 +1,3 @@
1
+ """AgentScout CLI — Command-line interface for AgentScout."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,256 @@
1
+ """Agent runner — wire up and execute tasks from the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ from agent_scout.agent import Agent
8
+ from agent_scout.config.settings import Settings
9
+ from agent_scout.db.models import AutonomyLevel, Task
10
+ from agent_scout.events import Event, EventBus, EventType
11
+ from agent_scout.llm.budget import BudgetConfig, BudgetEngine
12
+ from agent_scout.llm.estimator import CostEstimator
13
+ from agent_scout.llm.provider import (
14
+ AgentLLMProvider,
15
+ CustomEndpoint,
16
+ LLMClient,
17
+ ProviderConfig,
18
+ )
19
+ from agent_scout.llm.router import ModelRouter, RouterConfig
20
+ from agent_scout.plugins.registry import PluginRegistry
21
+ from agent_scout.plugins.web_search import WebSearchPlugin
22
+ from rich.console import Console
23
+
24
+ from agent_scout_cli.display.components import (
25
+ action_panel,
26
+ cost_display,
27
+ error_panel,
28
+ finish_panel,
29
+ observation_panel,
30
+ thought_panel,
31
+ )
32
+
33
+
34
+ def _build_provider_config(settings: Settings) -> ProviderConfig:
35
+ """Build ProviderConfig from Settings."""
36
+ endpoints = [
37
+ CustomEndpoint(
38
+ name=ep.name,
39
+ base_url=ep.base_url,
40
+ api_key=ep.api_key,
41
+ model_name=ep.model_name,
42
+ )
43
+ for ep in settings.custom_endpoints
44
+ ]
45
+ return ProviderConfig(
46
+ default_model=settings.llm.default_model,
47
+ temperature=settings.llm.temperature,
48
+ max_tokens=settings.llm.max_tokens,
49
+ custom_endpoints=endpoints,
50
+ )
51
+
52
+
53
+ def _build_router(settings: Settings) -> ModelRouter:
54
+ """Build ModelRouter from Settings."""
55
+ config = RouterConfig(
56
+ fast_model=settings.llm.fast_model,
57
+ balanced_model=settings.llm.default_model,
58
+ best_model=settings.llm.best_model,
59
+ enabled=settings.llm.enable_routing,
60
+ )
61
+ return ModelRouter(config)
62
+
63
+
64
+ def _build_budget(settings: Settings, bus: EventBus) -> BudgetEngine:
65
+ """Build BudgetEngine from Settings."""
66
+ config = BudgetConfig(
67
+ task_limit=settings.budget.task_limit,
68
+ daily_limit=settings.budget.daily_limit,
69
+ monthly_limit=settings.budget.monthly_limit,
70
+ enforce=settings.budget.enforce,
71
+ )
72
+ return BudgetEngine(config=config, event_bus=bus)
73
+
74
+
75
+ def _build_registry(settings: Settings) -> PluginRegistry:
76
+ """Build and populate the plugin registry."""
77
+ from agent_scout.plugins.base import PluginConfig
78
+
79
+ registry = PluginRegistry()
80
+
81
+ if "web_search" in settings.plugins:
82
+ ws_config = PluginConfig(
83
+ settings={
84
+ "tavily_api_key": settings.search.tavily_api_key,
85
+ "serpapi_api_key": settings.search.serpapi_api_key,
86
+ "brave_api_key": settings.search.brave_api_key,
87
+ }
88
+ )
89
+ registry.register(WebSearchPlugin(config=ws_config))
90
+
91
+ return registry
92
+
93
+
94
+ class CLIEventHandler:
95
+ """Display agent events in the terminal."""
96
+
97
+ def __init__(self, console: Console, verbose: bool = False) -> None:
98
+ self._console = console
99
+ self._verbose = verbose
100
+ self._step_count = 0
101
+ self._total_input = 0
102
+ self._total_output = 0
103
+ self._total_cost = 0.0
104
+
105
+ async def handle(self, event: Event) -> None:
106
+ """Route events to display methods."""
107
+ handlers = {
108
+ EventType.THOUGHT: self._on_thought,
109
+ EventType.ACTION: self._on_action,
110
+ EventType.OBSERVATION: self._on_observation,
111
+ EventType.TOOL_CALL_START: self._on_tool_start,
112
+ EventType.TOOL_CALL_END: self._on_tool_end,
113
+ EventType.TOOL_ERROR: self._on_tool_error,
114
+ EventType.COST_UPDATE: self._on_cost,
115
+ EventType.BUDGET_WARNING: self._on_budget_warning,
116
+ EventType.BUDGET_EXCEEDED: self._on_budget_exceeded,
117
+ EventType.TASK_COMPLETED: self._on_completed,
118
+ EventType.TASK_FAILED: self._on_failed,
119
+ EventType.ERROR: self._on_error,
120
+ }
121
+ handler = handlers.get(event.event_type)
122
+ if handler:
123
+ handler(event)
124
+
125
+ def _on_thought(self, event: Event) -> None:
126
+ self._step_count += 1
127
+ self._console.print(thought_panel(event.data.get("thought", ""), self._step_count))
128
+
129
+ def _on_action(self, event: Event) -> None:
130
+ tool = event.data.get("tool_name", "")
131
+ if tool:
132
+ self._console.print(action_panel(tool, event.data.get("tool_input")))
133
+
134
+ def _on_observation(self, event: Event) -> None:
135
+ obs = event.data.get("observation", "")
136
+ if obs and self._verbose:
137
+ self._console.print(observation_panel(obs))
138
+ elif obs:
139
+ short = obs[:200] + "..." if len(obs) > 200 else obs
140
+ self._console.print(f" [dim]{short}[/dim]")
141
+
142
+ def _on_tool_start(self, event: Event) -> None:
143
+ tool = event.data.get("tool_name", "")
144
+ self._console.print(f" [dim]Running {tool}...[/dim]")
145
+
146
+ def _on_tool_end(self, event: Event) -> None:
147
+ pass # observation handles the output
148
+
149
+ def _on_tool_error(self, event: Event) -> None:
150
+ err = event.data.get("error", "")
151
+ self._console.print(f" [red]Tool error: {err}[/red]")
152
+
153
+ def _on_cost(self, event: Event) -> None:
154
+ self._total_input += event.data.get("input_tokens", 0)
155
+ self._total_output += event.data.get("output_tokens", 0)
156
+ self._total_cost += event.data.get("cost_usd", 0.0)
157
+
158
+ def _on_budget_warning(self, event: Event) -> None:
159
+ pct = event.data.get("percentage", 0)
160
+ limit_type = event.data.get("limit_type", "")
161
+ self._console.print(f" [yellow]Budget warning: {limit_type} at {pct}%[/yellow]")
162
+
163
+ def _on_budget_exceeded(self, event: Event) -> None:
164
+ limit_type = event.data.get("limit_type", "")
165
+ self._console.print(f" [bold red]Budget exceeded: {limit_type}[/bold red]")
166
+
167
+ def _on_completed(self, event: Event) -> None:
168
+ result = event.data.get("result", {})
169
+ message = result.get("message", "") if isinstance(result, dict) else str(result)
170
+ if message:
171
+ self._console.print(finish_panel(message))
172
+ self._print_summary()
173
+
174
+ def _on_failed(self, event: Event) -> None:
175
+ err = event.data.get("error", "Task failed")
176
+ self._console.print(error_panel(err))
177
+ self._print_summary()
178
+
179
+ def _on_error(self, event: Event) -> None:
180
+ err = event.data.get("error", "")
181
+ self._console.print(f" [red]{err}[/red]")
182
+
183
+ def _print_summary(self) -> None:
184
+ if self._total_input > 0 or self._total_output > 0:
185
+ self._console.print(
186
+ cost_display(
187
+ self._total_input,
188
+ self._total_output,
189
+ self._total_cost,
190
+ )
191
+ )
192
+ self._console.print(f"[dim]Completed in {self._step_count} steps[/dim]")
193
+
194
+
195
+ async def run_task(
196
+ task_description: str,
197
+ settings: Settings,
198
+ console: Console,
199
+ model_override: str | None = None,
200
+ budget_override: float | None = None,
201
+ verbose: bool = False,
202
+ ) -> None:
203
+ """Build the full agent stack and execute a task."""
204
+ bus = EventBus()
205
+
206
+ # Event display
207
+ handler = CLIEventHandler(console, verbose=verbose)
208
+ bus.subscribe(handler.handle)
209
+
210
+ # LLM stack
211
+ provider_config = _build_provider_config(settings)
212
+ client = LLMClient(config=provider_config, event_bus=bus)
213
+ router = _build_router(settings)
214
+ estimator = CostEstimator()
215
+ budget = _build_budget(settings, bus)
216
+
217
+ if budget_override is not None:
218
+ budget.config.task_limit = budget_override
219
+
220
+ # Route to best model for this task
221
+ model = router.route(task_description, override_model=model_override)
222
+ llm_provider = AgentLLMProvider(client, model=model)
223
+
224
+ # Plugin registry
225
+ registry = _build_registry(settings)
226
+ await registry.load_all()
227
+
228
+ # Build autonomy level
229
+ autonomy_map = {
230
+ "full": AutonomyLevel.FULL,
231
+ "semi": AutonomyLevel.SEMI,
232
+ "step": AutonomyLevel.STEP_BY_STEP,
233
+ }
234
+ autonomy = autonomy_map.get(settings.autonomy, AutonomyLevel.FULL)
235
+
236
+ # Pre-execution cost estimate
237
+ estimate = estimator.estimate(model=model, input_tokens=500, expected_output_tokens=1000)
238
+ console.print(f"[dim]Model: {model} | Estimated cost: {estimate.formatted}[/dim]")
239
+
240
+ # Build and run agent
241
+ agent = Agent(
242
+ llm=llm_provider,
243
+ tools=registry,
244
+ event_bus=bus,
245
+ autonomy=autonomy,
246
+ )
247
+
248
+ task = Task(
249
+ id=uuid.uuid4().hex[:12],
250
+ description=task_description,
251
+ )
252
+
253
+ await agent.run_task(task)
254
+
255
+ # Cleanup
256
+ await registry.unload_all()
File without changes
@@ -0,0 +1,181 @@
1
+ """Rich display components for the AgentScout CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+
8
+
9
+ def thought_panel(thought: str, step_number: int) -> Panel:
10
+ """Display an agent thought."""
11
+ return Panel(
12
+ thought,
13
+ title=f"[bold cyan]Thought #{step_number}[/bold cyan]",
14
+ border_style="cyan",
15
+ padding=(0, 1),
16
+ )
17
+
18
+
19
+ def action_panel(tool_name: str, tool_input: dict | None = None) -> Panel:
20
+ """Display an agent action (tool call)."""
21
+ content = f"[bold]{tool_name}[/bold]"
22
+ if tool_input:
23
+ args = ", ".join(f"{k}={v!r}" for k, v in tool_input.items())
24
+ content += f"\n[dim]{args}[/dim]"
25
+ return Panel(
26
+ content,
27
+ title="[bold yellow]Action[/bold yellow]",
28
+ border_style="yellow",
29
+ padding=(0, 1),
30
+ )
31
+
32
+
33
+ def observation_panel(observation: str, max_length: int = 500) -> Panel:
34
+ """Display an observation (tool result)."""
35
+ display = observation[:max_length]
36
+ if len(observation) > max_length:
37
+ display += f"\n[dim]... ({len(observation) - max_length} chars truncated)[/dim]"
38
+ return Panel(
39
+ display,
40
+ title="[bold green]Observation[/bold green]",
41
+ border_style="green",
42
+ padding=(0, 1),
43
+ )
44
+
45
+
46
+ def finish_panel(message: str) -> Panel:
47
+ """Display the final result."""
48
+ return Panel(
49
+ message,
50
+ title="[bold green]Result[/bold green]",
51
+ border_style="bold green",
52
+ padding=(1, 2),
53
+ )
54
+
55
+
56
+ def error_panel(error: str) -> Panel:
57
+ """Display an error."""
58
+ return Panel(
59
+ f"[red]{error}[/red]",
60
+ title="[bold red]Error[/bold red]",
61
+ border_style="red",
62
+ padding=(0, 1),
63
+ )
64
+
65
+
66
+ def cost_display(
67
+ input_tokens: int,
68
+ output_tokens: int,
69
+ cost_usd: float,
70
+ budget_remaining: float | None = None,
71
+ ) -> Panel:
72
+ """Display cost information."""
73
+ lines = [
74
+ f"Tokens: [cyan]{input_tokens:,}[/cyan] in / [cyan]{output_tokens:,}[/cyan] out",
75
+ f"Cost: [bold]${cost_usd:.4f}[/bold]",
76
+ ]
77
+ if budget_remaining is not None:
78
+ color = "green" if budget_remaining > 0.5 else "yellow" if budget_remaining > 0.1 else "red"
79
+ lines.append(f"Budget remaining: [{color}]${budget_remaining:.2f}[/{color}]")
80
+ return Panel(
81
+ "\n".join(lines),
82
+ title="[bold]Cost[/bold]",
83
+ border_style="dim",
84
+ padding=(0, 1),
85
+ )
86
+
87
+
88
+ def comparison_table(
89
+ title: str,
90
+ columns: list[str],
91
+ rows: list[list[str]],
92
+ highlight_col: int | None = None,
93
+ highlight_row: int | None = 0,
94
+ ) -> Table:
95
+ """Build a comparison table with optional row highlighting."""
96
+ table = Table(title=title, show_lines=True)
97
+
98
+ for i, col in enumerate(columns):
99
+ style = "bold cyan" if i == highlight_col else ""
100
+ table.add_column(col, style=style)
101
+
102
+ for i, row in enumerate(rows):
103
+ style = "bold green" if i == highlight_row else ""
104
+ table.add_row(*row, style=style)
105
+
106
+ return table
107
+
108
+
109
+ def task_progress(
110
+ step_count: int,
111
+ max_steps: int,
112
+ current_action: str = "",
113
+ ) -> Panel:
114
+ """Display task progress."""
115
+ pct = min(100, int((step_count / max_steps) * 100)) if max_steps > 0 else 0
116
+ bar_filled = pct // 5
117
+ bar_empty = 20 - bar_filled
118
+ bar = f"[green]{'=' * bar_filled}[/green][dim]{'.' * bar_empty}[/dim]"
119
+
120
+ lines = [
121
+ f"Step {step_count}/{max_steps} [{bar}] {pct}%",
122
+ ]
123
+ if current_action:
124
+ lines.append(f"[dim]{current_action}[/dim]")
125
+
126
+ return Panel(
127
+ "\n".join(lines),
128
+ title="[bold]Progress[/bold]",
129
+ border_style="blue",
130
+ padding=(0, 1),
131
+ )
132
+
133
+
134
+ def sparkline(values: list[float], width: int = 20) -> str:
135
+ """Generate a simple text-based sparkline from values."""
136
+ if not values:
137
+ return ""
138
+
139
+ blocks = " ▁▂▃▄▅▆▇█"
140
+ mn, mx = min(values), max(values)
141
+ rng = mx - mn if mx != mn else 1
142
+
143
+ # Sample values to fit width
144
+ if len(values) > width:
145
+ step = len(values) / width
146
+ sampled = [values[int(i * step)] for i in range(width)]
147
+ else:
148
+ sampled = values
149
+
150
+ chars = []
151
+ for v in sampled:
152
+ idx = int(((v - mn) / rng) * (len(blocks) - 1))
153
+ chars.append(blocks[idx])
154
+ return "".join(chars)
155
+
156
+
157
+ def price_trend_display(
158
+ item_name: str,
159
+ prices: list[float],
160
+ currency: str = "USD",
161
+ ) -> Panel:
162
+ """Display a price trend with sparkline."""
163
+ if not prices:
164
+ return Panel(f"No price data for '{item_name}'", border_style="dim")
165
+
166
+ chart = sparkline(prices)
167
+ current = prices[-1]
168
+ mn = min(prices)
169
+ mx = max(prices)
170
+
171
+ lines = [
172
+ f"[bold]{item_name}[/bold]",
173
+ f"Current: {currency} {current:.2f}",
174
+ f"Range: {currency} {mn:.2f} - {currency} {mx:.2f}",
175
+ f"Trend: {chart}",
176
+ ]
177
+ return Panel(
178
+ "\n".join(lines),
179
+ border_style="cyan",
180
+ padding=(0, 1),
181
+ )
@@ -0,0 +1,445 @@
1
+ """AgentScout CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from agent_scout import __version__ as core_version
10
+ from agent_scout.config.settings import (
11
+ DEFAULT_CONFIG_FILE,
12
+ CustomEndpointSettings,
13
+ Settings,
14
+ load_settings,
15
+ save_settings,
16
+ )
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.prompt import Confirm, Prompt
20
+ from rich.table import Table
21
+
22
+ from . import __version__ as cli_version
23
+
24
+ app = typer.Typer(
25
+ name="agent-scout",
26
+ help="AgentScout — Your personal AI assistant agent that scouts the web for you.",
27
+ no_args_is_help=True,
28
+ )
29
+ console = Console()
30
+
31
+
32
+ def _load_settings() -> Settings:
33
+ """Load settings with error display."""
34
+ return load_settings()
35
+
36
+
37
+ # ---- run ----
38
+
39
+
40
+ @app.command()
41
+ def run(
42
+ task: str = typer.Argument(
43
+ help="Natural language description of what you want the agent to do",
44
+ ),
45
+ model: str | None = typer.Option(
46
+ None, "--model", "-m", help="LLM model to use (overrides routing)"
47
+ ),
48
+ budget: float | None = typer.Option(
49
+ None, "--budget", "-b", help="Max budget for this task (USD)"
50
+ ),
51
+ autonomy: str = typer.Option(None, "--autonomy", "-a", help="Autonomy level: full, semi, step"),
52
+ verbose: bool = typer.Option(
53
+ False, "--verbose", "-v", help="Show full observations and debug info"
54
+ ),
55
+ ):
56
+ """Run a scouting task."""
57
+ settings = _load_settings()
58
+
59
+ if autonomy:
60
+ settings.autonomy = autonomy
61
+ if verbose:
62
+ settings.verbose = True
63
+
64
+ console.print(
65
+ Panel(
66
+ f"[bold]Task:[/bold] {task}",
67
+ title="[bold green]AgentScout[/bold green]",
68
+ subtitle="Starting...",
69
+ border_style="green",
70
+ )
71
+ )
72
+
73
+ from agent_scout_cli.commands.runner import run_task
74
+
75
+ asyncio.run(
76
+ run_task(
77
+ task_description=task,
78
+ settings=settings,
79
+ console=console,
80
+ model_override=model,
81
+ budget_override=budget,
82
+ verbose=verbose,
83
+ )
84
+ )
85
+
86
+
87
+ # ---- setup ----
88
+
89
+
90
+ @app.command()
91
+ def setup(
92
+ reconfigure: bool = typer.Option(False, "--reconfigure", help="Reconfigure existing settings"),
93
+ ):
94
+ """Run the interactive setup wizard."""
95
+ settings = _load_settings() if reconfigure else Settings()
96
+
97
+ console.print(
98
+ Panel(
99
+ "[bold]Welcome to AgentScout![/bold]\n\n"
100
+ "This wizard will help you configure your AI assistant.\n"
101
+ "You can re-run this anytime with: [cyan]agent-scout setup --reconfigure[/cyan]",
102
+ title="[bold green]Setup Wizard[/bold green]",
103
+ border_style="green",
104
+ )
105
+ )
106
+
107
+ # Step 1: LLM model
108
+ console.print("\n[bold]Step 1: LLM Configuration[/bold]")
109
+ settings.llm.default_model = Prompt.ask(
110
+ "Default model",
111
+ default=settings.llm.default_model,
112
+ )
113
+
114
+ # Step 2: API keys (optional)
115
+ console.print("\n[bold]Step 2: Search API Keys[/bold] [dim](optional, Enter to skip)[/dim]")
116
+ settings.search.tavily_api_key = Prompt.ask(
117
+ "Tavily API key", default=settings.search.tavily_api_key or "", show_default=False
118
+ )
119
+ settings.search.serpapi_api_key = Prompt.ask(
120
+ "SerpAPI key", default=settings.search.serpapi_api_key or "", show_default=False
121
+ )
122
+ settings.search.brave_api_key = Prompt.ask(
123
+ "Brave Search key", default=settings.search.brave_api_key or "", show_default=False
124
+ )
125
+
126
+ # Step 3: Custom endpoints
127
+ console.print("\n[bold]Step 3: Custom LLM Endpoints[/bold]")
128
+ while Confirm.ask("Add a custom LLM endpoint?", default=False):
129
+ ep = CustomEndpointSettings(
130
+ name=Prompt.ask("Endpoint name (e.g., 'myapi')"),
131
+ base_url=Prompt.ask("Base URL"),
132
+ api_key=Prompt.ask("API key"),
133
+ model_name=Prompt.ask("Model name"),
134
+ )
135
+ settings.custom_endpoints.append(ep)
136
+
137
+ # Step 4: Budget
138
+ console.print("\n[bold]Step 4: Budget Limits[/bold]")
139
+ settings.budget.task_limit = float(
140
+ Prompt.ask("Max per-task spend (USD)", default=str(settings.budget.task_limit))
141
+ )
142
+ settings.budget.daily_limit = float(
143
+ Prompt.ask("Max daily spend (USD)", default=str(settings.budget.daily_limit))
144
+ )
145
+ settings.budget.monthly_limit = float(
146
+ Prompt.ask("Max monthly spend (USD)", default=str(settings.budget.monthly_limit))
147
+ )
148
+
149
+ # Step 5: Personality
150
+ console.print("\n[bold]Step 5: Personality[/bold]")
151
+ settings.personality = Prompt.ask(
152
+ "Agent personality",
153
+ choices=["helpful", "concise", "detailed", "casual"],
154
+ default=settings.personality,
155
+ )
156
+
157
+ # Save
158
+ save_settings(settings)
159
+ console.print(
160
+ Panel(
161
+ f"Configuration saved to [cyan]{DEFAULT_CONFIG_FILE}[/cyan]\n\n"
162
+ "You're all set! Run a task with:\n"
163
+ '[bold]agent-scout run "Find me the best deal on..."[/bold]',
164
+ title="[bold green]Setup Complete[/bold green]",
165
+ border_style="green",
166
+ )
167
+ )
168
+
169
+
170
+ # ---- config ----
171
+
172
+
173
+ @app.command()
174
+ def config(
175
+ action: str = typer.Argument("show", help="Action: show, set, add-endpoint"),
176
+ key: str | None = typer.Argument(None, help="Config key (for 'set')"),
177
+ value: str | None = typer.Argument(None, help="Config value (for 'set')"),
178
+ ):
179
+ """View or modify configuration."""
180
+ settings = _load_settings()
181
+
182
+ if action == "show":
183
+ _config_show(settings)
184
+ elif action == "set":
185
+ if not key or value is None:
186
+ console.print("[red]Usage: agent-scout config set <key> <value>[/red]")
187
+ raise typer.Exit(1)
188
+ _config_set(settings, key, value)
189
+ elif action == "add-endpoint":
190
+ _config_add_endpoint(settings)
191
+ else:
192
+ console.print(f"[red]Unknown action: {action}[/red]")
193
+ raise typer.Exit(1)
194
+
195
+
196
+ def _config_show(settings: Settings) -> None:
197
+ """Display current configuration."""
198
+ table = Table(title="AgentScout Configuration")
199
+ table.add_column("Setting", style="cyan")
200
+ table.add_column("Value", style="white")
201
+
202
+ table.add_row("Default model", settings.llm.default_model)
203
+ table.add_row("Fast model", settings.llm.fast_model)
204
+ table.add_row("Best model", settings.llm.best_model)
205
+ table.add_row("Temperature", str(settings.llm.temperature))
206
+ table.add_row("Max tokens", str(settings.llm.max_tokens))
207
+ table.add_row("Smart routing", str(settings.llm.enable_routing))
208
+ table.add_row("---", "---")
209
+ table.add_row("Task budget", f"${settings.budget.task_limit}")
210
+ table.add_row("Daily budget", f"${settings.budget.daily_limit}")
211
+ table.add_row("Monthly budget", f"${settings.budget.monthly_limit}")
212
+ table.add_row("Enforce budgets", str(settings.budget.enforce))
213
+ table.add_row("---", "---")
214
+ table.add_row("Personality", settings.personality)
215
+ table.add_row("Autonomy", settings.autonomy)
216
+ table.add_row("Plugins", ", ".join(settings.plugins))
217
+ table.add_row("---", "---")
218
+ table.add_row("Tavily key", "***" if settings.search.tavily_api_key else "(not set)")
219
+ table.add_row("SerpAPI key", "***" if settings.search.serpapi_api_key else "(not set)")
220
+ table.add_row("Brave key", "***" if settings.search.brave_api_key else "(not set)")
221
+
222
+ if settings.custom_endpoints:
223
+ table.add_row("---", "---")
224
+ for ep in settings.custom_endpoints:
225
+ table.add_row(f"Endpoint: {ep.name}", f"{ep.base_url} ({ep.model_name})")
226
+
227
+ console.print(table)
228
+ console.print(f"\n[dim]Config file: {DEFAULT_CONFIG_FILE}[/dim]")
229
+
230
+
231
+ def _config_set(settings: Settings, key: str, value: str) -> None:
232
+ """Set a single config value."""
233
+ parts = key.split(".")
234
+ try:
235
+ if len(parts) == 2:
236
+ section, field = parts
237
+ obj = getattr(settings, section, None)
238
+ if obj is None:
239
+ console.print(f"[red]Unknown section: {section}[/red]")
240
+ raise typer.Exit(1)
241
+ if not hasattr(obj, field):
242
+ console.print(f"[red]Unknown field: {section}.{field}[/red]")
243
+ raise typer.Exit(1)
244
+ current = getattr(obj, field)
245
+ # Type coercion
246
+ if isinstance(current, bool):
247
+ setattr(obj, field, value.lower() in ("true", "1", "yes"))
248
+ elif isinstance(current, float):
249
+ setattr(obj, field, float(value))
250
+ elif isinstance(current, int):
251
+ setattr(obj, field, int(value))
252
+ else:
253
+ setattr(obj, field, value)
254
+ elif len(parts) == 1:
255
+ if not hasattr(settings, key):
256
+ console.print(f"[red]Unknown key: {key}[/red]")
257
+ raise typer.Exit(1)
258
+ setattr(settings, key, value)
259
+ else:
260
+ console.print(f"[red]Invalid key format: {key}[/red]")
261
+ raise typer.Exit(1)
262
+
263
+ save_settings(settings)
264
+ console.print(f"[green]Set {key} = {value}[/green]")
265
+
266
+ except (ValueError, TypeError) as e:
267
+ console.print(f"[red]Invalid value: {e}[/red]")
268
+ raise typer.Exit(1) from e
269
+
270
+
271
+ def _config_add_endpoint(settings: Settings) -> None:
272
+ """Interactively add a custom endpoint."""
273
+ ep = CustomEndpointSettings(
274
+ name=Prompt.ask("Endpoint name"),
275
+ base_url=Prompt.ask("Base URL"),
276
+ api_key=Prompt.ask("API key"),
277
+ model_name=Prompt.ask("Model name"),
278
+ )
279
+ settings.custom_endpoints.append(ep)
280
+ save_settings(settings)
281
+ console.print(f"[green]Added endpoint '{ep.name}'[/green]")
282
+
283
+
284
+ # ---- history ----
285
+
286
+
287
+ @app.command()
288
+ def history(
289
+ task_id: str | None = typer.Argument(None, help="Task ID to view details"),
290
+ search: str | None = typer.Option(None, "--search", "-s", help="Search task descriptions"),
291
+ export_format: str | None = typer.Option(
292
+ None, "--export", "-e", help="Export format: json, markdown, csv"
293
+ ),
294
+ ):
295
+ """View task history."""
296
+ console.print("[bold]Task History[/bold]")
297
+
298
+ from agent_scout.config.settings import DEFAULT_CONFIG_DIR
299
+ from agent_scout.db.database import Database
300
+
301
+ db_path = Path(DEFAULT_CONFIG_DIR) / "agent_scout.db"
302
+
303
+ if not db_path.exists():
304
+ console.print("[dim]No task history yet. Run a task first![/dim]")
305
+ return
306
+
307
+ async def _show_history() -> None:
308
+ db = Database(str(db_path))
309
+ await db.connect()
310
+ try:
311
+ tasks = await db.list_tasks()
312
+
313
+ if search:
314
+ tasks = [t for t in tasks if search.lower() in t.description.lower()]
315
+
316
+ if task_id:
317
+ task = await db.get_task(task_id)
318
+ if task:
319
+ _display_task_detail(task)
320
+ else:
321
+ console.print(f"[red]Task '{task_id}' not found[/red]")
322
+ return
323
+
324
+ if not tasks:
325
+ console.print("[dim]No tasks found.[/dim]")
326
+ return
327
+
328
+ table = Table(title="Task History")
329
+ table.add_column("ID", style="cyan", max_width=12)
330
+ table.add_column("Description", max_width=50)
331
+ table.add_column("Status", style="bold")
332
+ table.add_column("Created")
333
+
334
+ for t in tasks[:50]:
335
+ status_color = {
336
+ "completed": "green",
337
+ "failed": "red",
338
+ "executing": "yellow",
339
+ }.get(t.status.value, "white")
340
+
341
+ import time
342
+
343
+ created = time.strftime("%Y-%m-%d %H:%M", time.localtime(t.created_at))
344
+ table.add_row(
345
+ t.id[:12],
346
+ t.description[:50],
347
+ f"[{status_color}]{t.status.value}[/{status_color}]",
348
+ created,
349
+ )
350
+
351
+ console.print(table)
352
+ finally:
353
+ await db.disconnect()
354
+
355
+ asyncio.run(_show_history())
356
+
357
+
358
+ def _display_task_detail(task) -> None: # noqa: ANN001
359
+ """Display detailed task information."""
360
+ import time
361
+
362
+ created = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(task.created_at))
363
+ updated = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(task.updated_at))
364
+
365
+ console.print(
366
+ Panel(
367
+ f"[bold]ID:[/bold] {task.id}\n"
368
+ f"[bold]Description:[/bold] {task.description}\n"
369
+ f"[bold]Status:[/bold] {task.status.value}\n"
370
+ f"[bold]Created:[/bold] {created}\n"
371
+ f"[bold]Updated:[/bold] {updated}\n"
372
+ f"[bold]Result:[/bold] {task.result or '(none)'}\n"
373
+ f"[bold]Error:[/bold] {task.error or '(none)'}",
374
+ title="[bold]Task Detail[/bold]",
375
+ )
376
+ )
377
+
378
+
379
+ # ---- schedule ----
380
+
381
+
382
+ @app.command()
383
+ def schedule(
384
+ action: str = typer.Argument("list", help="Action: list, add, remove"),
385
+ schedule_id: str | None = typer.Argument(None, help="Schedule ID (for remove)"),
386
+ task_desc: str | None = typer.Option(None, "--task", "-t", help="Task description"),
387
+ cron: str | None = typer.Option(None, "--cron", "-c", help="Cron expression"),
388
+ ):
389
+ """Manage scheduled tasks."""
390
+ if action == "list":
391
+ console.print("[bold]Scheduled Tasks[/bold]")
392
+ console.print("[dim]No scheduled tasks. Add one with:[/dim]")
393
+ console.print(
394
+ '[dim]agent-scout schedule add --task "Check prices" --cron "0 8 * * *"[/dim]'
395
+ )
396
+ elif action == "add":
397
+ if not task_desc or not cron:
398
+ console.print("[red]Usage: agent-scout schedule add --task '...' --cron '...'[/red]")
399
+ raise typer.Exit(1)
400
+ console.print(f"[green]Scheduled: '{task_desc}' with cron '{cron}'[/green]")
401
+ console.print("[yellow]Note: Scheduling persistence coming in Phase 7[/yellow]")
402
+ elif action == "remove":
403
+ if not schedule_id:
404
+ console.print("[red]Usage: agent-scout schedule remove <schedule-id>[/red]")
405
+ raise typer.Exit(1)
406
+ console.print(f"[green]Removed schedule '{schedule_id}'[/green]")
407
+ else:
408
+ console.print(f"[red]Unknown action: {action}[/red]")
409
+ raise typer.Exit(1)
410
+
411
+
412
+ # ---- status ----
413
+
414
+
415
+ @app.command()
416
+ def status():
417
+ """Show agent status and cost summary."""
418
+ settings = _load_settings()
419
+
420
+ console.print(
421
+ Panel(
422
+ f"[bold]Model:[/bold] {settings.llm.default_model}\n"
423
+ f"[bold]Routing:[/bold] {'enabled' if settings.llm.enable_routing else 'disabled'}\n"
424
+ f"[bold]Autonomy:[/bold] {settings.autonomy}\n"
425
+ f"[bold]Budget:[/bold] ${settings.budget.task_limit}/task, "
426
+ f"${settings.budget.daily_limit}/day, "
427
+ f"${settings.budget.monthly_limit}/month\n"
428
+ f"[bold]Plugins:[/bold] {', '.join(settings.plugins)}",
429
+ title="[bold green]AgentScout Status[/bold green]",
430
+ border_style="green",
431
+ )
432
+ )
433
+
434
+
435
+ # ---- version ----
436
+
437
+
438
+ @app.command()
439
+ def version():
440
+ """Show version information."""
441
+ console.print(f"[bold]AgentScout[/bold] CLI v{cli_version} | Core v{core_version}")
442
+
443
+
444
+ if __name__ == "__main__":
445
+ app()
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-scout-cli
3
+ Version: 0.1.0
4
+ Summary: CLI interface for AgentScout — your personal AI assistant agent
5
+ Author: AgentScout Contributors
6
+ License-Expression: AGPL-3.0-or-later
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: agent-scout-core
9
+ Requires-Dist: rich>=13
10
+ Requires-Dist: typer>=0.24
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: ruff>=0.8; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # agent-scout-cli
17
+
18
+ CLI interface for AgentScout — your personal AI assistant agent.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ agent-scout run "Find me the best flight from NYC to London on Feb 28"
24
+ agent-scout setup
25
+ agent-scout config show
26
+ agent-scout history
27
+ agent-scout schedule list
28
+ agent-scout web
29
+ ```
@@ -0,0 +1,10 @@
1
+ agent_scout_cli/__init__.py,sha256=2rlDEZnw95ylM3JIK-INxpQJvyNjH-oFyYq0_uI7urs,87
2
+ agent_scout_cli/main.py,sha256=ZY5vWRlwIKDa1wTeu1ZekXC08uGgCFDeWc3xYGoQlD8,15063
3
+ agent_scout_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ agent_scout_cli/commands/runner.py,sha256=qGLk8pLNy5WId4PnnwD6NG8PbaiJEcSLXnk4YtGGPQQ,8613
5
+ agent_scout_cli/display/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ agent_scout_cli/display/components.py,sha256=_AHH8Q5kmTu_pQvxdTbsRcrecRgSwCIq1fmAOoXL6I4,4939
7
+ agent_scout_cli-0.1.0.dist-info/METADATA,sha256=nALbGjGIwX85_PydvOOD79aaCeprFtfhcnPThVenN9k,738
8
+ agent_scout_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ agent_scout_cli-0.1.0.dist-info/entry_points.txt,sha256=pij6m7OIa1ibq9o2Tnq1frz43k6Z5_cdWMp6uc6d1zo,57
10
+ agent_scout_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-scout = agent_scout_cli.main:app