tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/cli/mcp.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""MCP server management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
mcp_app = typer.Typer(help="Manage MCP server configurations")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp_app.command("list")
|
|
14
|
+
def mcp_list():
|
|
15
|
+
"""List all configured MCP servers."""
|
|
16
|
+
from tsugite.mcp_config import get_default_config_path, load_mcp_config
|
|
17
|
+
|
|
18
|
+
config_path = get_default_config_path()
|
|
19
|
+
servers = load_mcp_config()
|
|
20
|
+
|
|
21
|
+
if not servers:
|
|
22
|
+
console.print("[yellow]No MCP servers configured[/yellow]")
|
|
23
|
+
console.print(f"\nConfig file will be created at: {config_path}")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
console.print(f"[cyan]Found {len(servers)} MCP server(s)[/cyan] in [dim]{config_path}[/dim]\n")
|
|
27
|
+
|
|
28
|
+
for name, config in servers.items():
|
|
29
|
+
server_type = "stdio" if config.is_stdio() else "HTTP"
|
|
30
|
+
console.print(f"[bold]{name}[/bold] ({server_type})")
|
|
31
|
+
|
|
32
|
+
if config.is_stdio():
|
|
33
|
+
console.print(f" Command: {config.command}")
|
|
34
|
+
if config.args:
|
|
35
|
+
console.print(f" Args: {' '.join(config.args)}")
|
|
36
|
+
if config.env:
|
|
37
|
+
env_keys = list(config.env.keys())
|
|
38
|
+
console.print(f" Env vars: {', '.join(env_keys)}")
|
|
39
|
+
else:
|
|
40
|
+
console.print(f" URL: {config.url}")
|
|
41
|
+
|
|
42
|
+
console.print()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp_app.command("show")
|
|
46
|
+
def mcp_show(server_name: str = typer.Argument(help="Name of the MCP server to show")):
|
|
47
|
+
"""Show detailed configuration for a specific MCP server."""
|
|
48
|
+
from rich.panel import Panel
|
|
49
|
+
|
|
50
|
+
from tsugite.mcp_config import load_mcp_config
|
|
51
|
+
|
|
52
|
+
servers = load_mcp_config()
|
|
53
|
+
|
|
54
|
+
if server_name not in servers:
|
|
55
|
+
console.print(f"[red]MCP server '{server_name}' not found[/red]")
|
|
56
|
+
console.print(f"\nAvailable servers: {', '.join(servers.keys())}")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
config = servers[server_name]
|
|
60
|
+
server_type = "stdio" if config.is_stdio() else "HTTP"
|
|
61
|
+
|
|
62
|
+
console.print(Panel(f"[cyan]Server:[/cyan] {server_name}\n[cyan]Type:[/cyan] {server_type}", border_style="blue"))
|
|
63
|
+
|
|
64
|
+
if config.is_stdio():
|
|
65
|
+
console.print(f"\n[bold]Command:[/bold] {config.command}")
|
|
66
|
+
if config.args:
|
|
67
|
+
console.print("[bold]Arguments:[/bold]")
|
|
68
|
+
for arg in config.args:
|
|
69
|
+
console.print(f" - {arg}")
|
|
70
|
+
if config.env:
|
|
71
|
+
console.print("\n[bold]Environment Variables:[/bold]")
|
|
72
|
+
for key, value in config.env.items():
|
|
73
|
+
if "token" in key.lower() or "key" in key.lower() or "secret" in key.lower():
|
|
74
|
+
console.print(f" {key}: [dim]<redacted>[/dim]")
|
|
75
|
+
else:
|
|
76
|
+
console.print(f" {key}: {value}")
|
|
77
|
+
else:
|
|
78
|
+
console.print(f"\n[bold]URL:[/bold] {config.url}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@mcp_app.command("test")
|
|
82
|
+
def mcp_test(
|
|
83
|
+
server_name: str = typer.Argument(help="Name of the MCP server to test"),
|
|
84
|
+
):
|
|
85
|
+
"""Test connection to an MCP server and list available tools."""
|
|
86
|
+
import asyncio
|
|
87
|
+
|
|
88
|
+
from tsugite.mcp_client import load_mcp_tools
|
|
89
|
+
from tsugite.mcp_config import load_mcp_config
|
|
90
|
+
|
|
91
|
+
servers = load_mcp_config()
|
|
92
|
+
|
|
93
|
+
if server_name not in servers:
|
|
94
|
+
console.print(f"[red]MCP server '{server_name}' not found[/red]")
|
|
95
|
+
console.print(f"\nAvailable servers: {', '.join(servers.keys())}")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
config = servers[server_name]
|
|
99
|
+
|
|
100
|
+
console.print(f"[cyan]Testing connection to '{server_name}'...[/cyan]")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Use new async mcp_client implementation
|
|
104
|
+
tools = asyncio.run(load_mcp_tools(config, allowed_tools=None))
|
|
105
|
+
|
|
106
|
+
console.print(f"[green]✓ Successfully connected to '{server_name}'[/green]")
|
|
107
|
+
console.print(f"\n[bold]Available tools ({len(tools)}):[/bold]")
|
|
108
|
+
|
|
109
|
+
for tool in tools:
|
|
110
|
+
console.print(f" - {tool.name}: {tool.description}")
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
console.print(f"[red]✗ Connection failed: {e}[/red]")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp_app.command("add")
|
|
118
|
+
def mcp_add(
|
|
119
|
+
name: str = typer.Argument(help="Name for the MCP server"),
|
|
120
|
+
url: Optional[str] = typer.Option(None, "--url", help="URL for HTTP server"),
|
|
121
|
+
command: Optional[str] = typer.Option(None, "--command", help="Command for stdio server"),
|
|
122
|
+
args: Optional[list[str]] = typer.Option(None, "--args", help="Argument for stdio server (repeatable)"),
|
|
123
|
+
env: Optional[list[str]] = typer.Option(None, "--env", help="Environment variable as KEY=value (repeatable)"),
|
|
124
|
+
server_type: Optional[str] = typer.Option(None, "--type", help="Server type: stdio or http"),
|
|
125
|
+
force: bool = typer.Option(False, "--force", help="Overwrite if server already exists"),
|
|
126
|
+
):
|
|
127
|
+
"""Add a new MCP server to the configuration."""
|
|
128
|
+
from tsugite.mcp_config import MCPServerConfig, add_server_to_config
|
|
129
|
+
|
|
130
|
+
# Validate that either url or command is provided
|
|
131
|
+
if not url and not command:
|
|
132
|
+
console.print("[red]Error: Must specify either --url (for HTTP) or --command (for stdio)[/red]")
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
|
|
135
|
+
if url and command:
|
|
136
|
+
console.print("[red]Error: Cannot specify both --url and --command[/red]")
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
|
|
139
|
+
# Parse environment variables
|
|
140
|
+
env_dict = None
|
|
141
|
+
if env:
|
|
142
|
+
env_dict = {}
|
|
143
|
+
for env_var in env:
|
|
144
|
+
if "=" not in env_var:
|
|
145
|
+
console.print(f"[red]Error: Invalid env var format '{env_var}'. Must be KEY=value[/red]")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
key, value = env_var.split("=", 1)
|
|
148
|
+
env_dict[key] = value
|
|
149
|
+
|
|
150
|
+
# Create server config
|
|
151
|
+
try:
|
|
152
|
+
server = MCPServerConfig(
|
|
153
|
+
name=name,
|
|
154
|
+
url=url,
|
|
155
|
+
command=command,
|
|
156
|
+
args=args if args else None,
|
|
157
|
+
env=env_dict,
|
|
158
|
+
type=server_type,
|
|
159
|
+
)
|
|
160
|
+
except ValueError as e:
|
|
161
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
|
|
164
|
+
# Add to config
|
|
165
|
+
try:
|
|
166
|
+
from tsugite.mcp_config import get_config_path_for_write
|
|
167
|
+
|
|
168
|
+
config_path = get_config_path_for_write()
|
|
169
|
+
add_server_to_config(server, overwrite=force)
|
|
170
|
+
|
|
171
|
+
action = "Updated" if force else "Added"
|
|
172
|
+
server_type_name = "HTTP" if server.is_http() else "stdio"
|
|
173
|
+
|
|
174
|
+
console.print(f"[green]✓ {action} MCP server '{name}' ({server_type_name})[/green]")
|
|
175
|
+
|
|
176
|
+
# Show config summary
|
|
177
|
+
if server.is_http():
|
|
178
|
+
console.print(f" URL: {server.url}")
|
|
179
|
+
else:
|
|
180
|
+
console.print(f" Command: {server.command}")
|
|
181
|
+
if server.args:
|
|
182
|
+
console.print(f" Args: {' '.join(server.args)}")
|
|
183
|
+
if server.env:
|
|
184
|
+
console.print(f" Env vars: {', '.join(server.env.keys())}")
|
|
185
|
+
|
|
186
|
+
console.print(f"\nServer saved to: {config_path}")
|
|
187
|
+
|
|
188
|
+
except ValueError as e:
|
|
189
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
190
|
+
raise typer.Exit(1)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
console.print(f"[red]Failed to add server: {e}[/red]")
|
|
193
|
+
raise typer.Exit(1)
|
tsugite/cli/tools.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Tools management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
tools_app = typer.Typer(help="Manage and inspect available tools")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@tools_app.command("list")
|
|
15
|
+
def tools_list(
|
|
16
|
+
category: Optional[str] = typer.Option(None, "--category", help="Filter by category (fs, shell, http, etc.)"),
|
|
17
|
+
):
|
|
18
|
+
"""List all available tools."""
|
|
19
|
+
from tsugite.tools import _tools
|
|
20
|
+
|
|
21
|
+
if not _tools:
|
|
22
|
+
console.print("[yellow]No tools registered[/yellow]")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# Group tools by module
|
|
26
|
+
categorized = {}
|
|
27
|
+
for tool_name, tool_info in _tools.items():
|
|
28
|
+
module = tool_info.func.__module__.split(".")[-1] # Get last part (e.g., 'fs' from 'tsugite.tools.fs')
|
|
29
|
+
if module not in categorized:
|
|
30
|
+
categorized[module] = []
|
|
31
|
+
categorized[module].append((tool_name, tool_info.description))
|
|
32
|
+
|
|
33
|
+
# Filter by category if specified
|
|
34
|
+
if category:
|
|
35
|
+
if category not in categorized:
|
|
36
|
+
console.print(f"[red]Category '{category}' not found[/red]")
|
|
37
|
+
console.print(f"\nAvailable categories: {', '.join(sorted(categorized.keys()))}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
categorized = {category: categorized[category]}
|
|
40
|
+
|
|
41
|
+
# Display tools by category
|
|
42
|
+
total_count = 0
|
|
43
|
+
for cat in sorted(categorized.keys()):
|
|
44
|
+
tools = sorted(categorized[cat])
|
|
45
|
+
total_count += len(tools)
|
|
46
|
+
|
|
47
|
+
console.print(f"\n[cyan]{cat.upper()}[/cyan] ({len(tools)} tool{'s' if len(tools) != 1 else ''})")
|
|
48
|
+
console.print("[dim]" + "─" * 60 + "[/dim]")
|
|
49
|
+
|
|
50
|
+
for tool_name, description in tools:
|
|
51
|
+
console.print(f" [bold]{tool_name}[/bold]")
|
|
52
|
+
console.print(f" {description}")
|
|
53
|
+
|
|
54
|
+
console.print(f"\n[dim]Total: {total_count} tool{'s' if total_count != 1 else ''}[/dim]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@tools_app.command("show")
|
|
58
|
+
def tools_show(
|
|
59
|
+
tool_name: str = typer.Argument(help="Tool name to inspect"),
|
|
60
|
+
):
|
|
61
|
+
"""Show detailed information about a specific tool."""
|
|
62
|
+
from tsugite.tools import _tools
|
|
63
|
+
|
|
64
|
+
if tool_name not in _tools:
|
|
65
|
+
console.print(f"[red]Tool '{tool_name}' not found[/red]")
|
|
66
|
+
console.print("\nUse [cyan]tsugite tools list[/cyan] to see all available tools")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
tool_info = _tools[tool_name]
|
|
70
|
+
|
|
71
|
+
# Header
|
|
72
|
+
console.print(f"\n[bold cyan]{tool_info.name}[/bold cyan]")
|
|
73
|
+
console.print(f"[dim]{tool_info.description}[/dim]\n")
|
|
74
|
+
|
|
75
|
+
# Parameters table
|
|
76
|
+
if tool_info.parameters:
|
|
77
|
+
table = Table(show_header=True, header_style="bold cyan", border_style="dim")
|
|
78
|
+
table.add_column("Parameter", style="bold")
|
|
79
|
+
table.add_column("Type")
|
|
80
|
+
table.add_column("Required")
|
|
81
|
+
table.add_column("Default")
|
|
82
|
+
|
|
83
|
+
for param_name, param_info in tool_info.parameters.items():
|
|
84
|
+
param_type = (
|
|
85
|
+
param_info["type"].__name__ if hasattr(param_info["type"], "__name__") else str(param_info["type"])
|
|
86
|
+
)
|
|
87
|
+
required = "✓" if param_info["required"] else ""
|
|
88
|
+
default = "" if param_info["default"] is None else str(param_info["default"])
|
|
89
|
+
|
|
90
|
+
table.add_row(param_name, param_type, required, default)
|
|
91
|
+
|
|
92
|
+
console.print(table)
|
|
93
|
+
else:
|
|
94
|
+
console.print("[dim]No parameters[/dim]")
|
|
95
|
+
|
|
96
|
+
# Module info
|
|
97
|
+
module = tool_info.func.__module__
|
|
98
|
+
console.print(f"\n[dim]Module: {module}[/dim]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@tools_app.command("add")
|
|
102
|
+
def tools_add(
|
|
103
|
+
name: str = typer.Argument(help="Tool name"),
|
|
104
|
+
command: str = typer.Option(..., "--command", "-c", help="Shell command template (e.g., 'rg {pattern} {path}')"),
|
|
105
|
+
description: str = typer.Option("", "--description", "-d", help="Tool description"),
|
|
106
|
+
param: Optional[list[str]] = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--param",
|
|
109
|
+
"-p",
|
|
110
|
+
help="Parameter: name[:type][:required|default]. Type defaults to 'str' if omitted.",
|
|
111
|
+
),
|
|
112
|
+
timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout in seconds"),
|
|
113
|
+
):
|
|
114
|
+
"""Add a custom shell tool.
|
|
115
|
+
|
|
116
|
+
Parameter formats (type defaults to str):
|
|
117
|
+
-p pattern # str, optional
|
|
118
|
+
-p pattern:required # str, required
|
|
119
|
+
-p path:. # str with default value "."
|
|
120
|
+
-p count:int # int, optional
|
|
121
|
+
-p count:int:required # int, required
|
|
122
|
+
-p count:int:10 # int with default value 10
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
tsugite tools add file_search -c "rg {pattern} {path}" -p pattern:required -p path:.
|
|
126
|
+
tsugite tools add git_search -c "git log --all --grep={query}" -p query:required
|
|
127
|
+
tsugite tools add count_lines -c "wc -l {file}" -p file:required
|
|
128
|
+
"""
|
|
129
|
+
from tsugite.shell_tool_config import load_custom_tools_config, save_custom_tools_config
|
|
130
|
+
from tsugite.tools.shell_tools import ShellToolDefinition, ShellToolParameter
|
|
131
|
+
|
|
132
|
+
# Parse parameters
|
|
133
|
+
parameters = {}
|
|
134
|
+
if param:
|
|
135
|
+
for param_spec in param:
|
|
136
|
+
parts = param_spec.split(":")
|
|
137
|
+
if len(parts) < 1:
|
|
138
|
+
console.print(f"[red]Invalid parameter spec: {param_spec}[/red]")
|
|
139
|
+
console.print("Format: name[:type][:required|default]")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
param_name = parts[0]
|
|
143
|
+
|
|
144
|
+
# Default to string type
|
|
145
|
+
param_type = "str"
|
|
146
|
+
required = False
|
|
147
|
+
default = None
|
|
148
|
+
|
|
149
|
+
if len(parts) >= 2:
|
|
150
|
+
# Second part is either type or default/required
|
|
151
|
+
second = parts[1]
|
|
152
|
+
if second in ("str", "int", "bool", "float"):
|
|
153
|
+
# It's a type
|
|
154
|
+
param_type = second
|
|
155
|
+
|
|
156
|
+
# Check for third part (required or default)
|
|
157
|
+
if len(parts) >= 3:
|
|
158
|
+
if parts[2] == "required":
|
|
159
|
+
required = True
|
|
160
|
+
else:
|
|
161
|
+
default = parts[2]
|
|
162
|
+
elif second == "required":
|
|
163
|
+
# No type specified, but marked as required
|
|
164
|
+
required = True
|
|
165
|
+
else:
|
|
166
|
+
# It's a default value (type defaults to str)
|
|
167
|
+
default = second
|
|
168
|
+
|
|
169
|
+
parameters[param_name] = ShellToolParameter(
|
|
170
|
+
name=param_name,
|
|
171
|
+
type=param_type,
|
|
172
|
+
description="",
|
|
173
|
+
required=required,
|
|
174
|
+
default=default,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Create tool definition
|
|
178
|
+
tool_def = ShellToolDefinition(
|
|
179
|
+
name=name,
|
|
180
|
+
description=description or f"Custom shell tool: {name}",
|
|
181
|
+
command=command,
|
|
182
|
+
parameters=parameters,
|
|
183
|
+
timeout=timeout,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Load existing tools
|
|
187
|
+
try:
|
|
188
|
+
existing_tools = load_custom_tools_config()
|
|
189
|
+
|
|
190
|
+
# Check for duplicates
|
|
191
|
+
if any(t.name == name for t in existing_tools):
|
|
192
|
+
console.print(f"[red]Tool '{name}' already exists[/red]")
|
|
193
|
+
console.print("Use [cyan]tsugite tools remove[/cyan] first or choose a different name")
|
|
194
|
+
raise typer.Exit(1)
|
|
195
|
+
|
|
196
|
+
# Add new tool
|
|
197
|
+
existing_tools.append(tool_def)
|
|
198
|
+
|
|
199
|
+
# Save
|
|
200
|
+
save_custom_tools_config(existing_tools)
|
|
201
|
+
|
|
202
|
+
console.print(f"[green]✓[/green] Added custom tool: [bold]{name}[/bold]")
|
|
203
|
+
console.print(f"\nCommand: {command}")
|
|
204
|
+
if parameters:
|
|
205
|
+
console.print("\nParameters:")
|
|
206
|
+
for param_name, param_def in parameters.items():
|
|
207
|
+
req_str = " (required)" if param_def.required else f" (default: {param_def.default})"
|
|
208
|
+
console.print(f" • {param_name}: {param_def.type}{req_str}")
|
|
209
|
+
|
|
210
|
+
console.print(f'\n[dim]Try it: tsugite run +assistant "use {name} to..."[/dim]')
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
console.print(f"[red]Failed to add tool: {e}[/red]")
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@tools_app.command("remove")
|
|
218
|
+
def tools_remove(
|
|
219
|
+
name: str = typer.Argument(help="Tool name to remove"),
|
|
220
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
221
|
+
):
|
|
222
|
+
"""Remove a custom shell tool."""
|
|
223
|
+
from tsugite.shell_tool_config import load_custom_tools_config, save_custom_tools_config
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
existing_tools = load_custom_tools_config()
|
|
227
|
+
|
|
228
|
+
# Find tool
|
|
229
|
+
tool_def = next((t for t in existing_tools if t.name == name), None)
|
|
230
|
+
if not tool_def:
|
|
231
|
+
console.print(f"[red]Custom tool '{name}' not found[/red]")
|
|
232
|
+
console.print("\nUse [cyan]tsugite tools list --custom[/cyan] to see custom tools")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
# Confirm
|
|
236
|
+
if not yes:
|
|
237
|
+
console.print(f"\nRemove custom tool [bold]{name}[/bold]?")
|
|
238
|
+
console.print(f"Command: {tool_def.command}")
|
|
239
|
+
confirm = typer.confirm("Continue?")
|
|
240
|
+
if not confirm:
|
|
241
|
+
console.print("Cancelled")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# Remove
|
|
245
|
+
existing_tools = [t for t in existing_tools if t.name != name]
|
|
246
|
+
save_custom_tools_config(existing_tools)
|
|
247
|
+
|
|
248
|
+
console.print(f"[green]✓[/green] Removed custom tool: [bold]{name}[/bold]")
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
console.print(f"[red]Failed to remove tool: {e}[/red]")
|
|
252
|
+
raise typer.Exit(1)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@tools_app.command("edit")
|
|
256
|
+
def tools_edit(
|
|
257
|
+
name: str = typer.Argument(help="Tool name to edit"),
|
|
258
|
+
):
|
|
259
|
+
"""Edit a custom shell tool in your editor."""
|
|
260
|
+
import os
|
|
261
|
+
import subprocess
|
|
262
|
+
|
|
263
|
+
from tsugite.shell_tool_config import get_custom_tools_config_path
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
config_path = get_custom_tools_config_path()
|
|
267
|
+
|
|
268
|
+
if not config_path.exists():
|
|
269
|
+
console.print("[yellow]No custom tools config found[/yellow]")
|
|
270
|
+
console.print("Add a tool first with [cyan]tsugite tools add[/cyan]")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# Open in editor
|
|
274
|
+
editor = os.environ.get("EDITOR", "nano")
|
|
275
|
+
subprocess.run([editor, str(config_path)], check=False)
|
|
276
|
+
|
|
277
|
+
console.print("[green]✓[/green] Config updated")
|
|
278
|
+
console.print("\n[dim]Restart tsugite to load changes[/dim]")
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]Failed to edit tools: {e}[/red]")
|
|
282
|
+
raise typer.Exit(1)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@tools_app.command("validate")
|
|
286
|
+
def tools_validate():
|
|
287
|
+
"""Validate custom_tools.yaml configuration."""
|
|
288
|
+
from tsugite.shell_tool_config import get_custom_tools_config_path, load_custom_tools_config
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
config_path = get_custom_tools_config_path()
|
|
292
|
+
|
|
293
|
+
if not config_path.exists():
|
|
294
|
+
console.print("[yellow]No custom tools configuration found[/yellow]")
|
|
295
|
+
console.print(f"\nExpected location: {config_path}")
|
|
296
|
+
console.print("\nUse [cyan]tsugite tools add[/cyan] to create custom tools")
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
console.print(f"Validating: {config_path}\n")
|
|
300
|
+
|
|
301
|
+
# Try to load the configuration
|
|
302
|
+
definitions = load_custom_tools_config()
|
|
303
|
+
|
|
304
|
+
if not definitions:
|
|
305
|
+
console.print("[yellow]⚠[/yellow] Configuration is valid but no tools are defined")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Validate each tool definition
|
|
309
|
+
console.print(f"[green]✓[/green] Found {len(definitions)} tool definition(s)\n")
|
|
310
|
+
|
|
311
|
+
table = Table(show_header=True, header_style="bold cyan", border_style="dim")
|
|
312
|
+
table.add_column("Tool Name", style="bold")
|
|
313
|
+
table.add_column("Command")
|
|
314
|
+
table.add_column("Parameters")
|
|
315
|
+
table.add_column("Status")
|
|
316
|
+
|
|
317
|
+
for tool_def in definitions:
|
|
318
|
+
param_count = len(tool_def.parameters)
|
|
319
|
+
param_str = f"{param_count} parameter(s)"
|
|
320
|
+
|
|
321
|
+
# Check for potential issues
|
|
322
|
+
issues = []
|
|
323
|
+
if not tool_def.description:
|
|
324
|
+
issues.append("no description")
|
|
325
|
+
if not tool_def.parameters:
|
|
326
|
+
issues.append("no parameters")
|
|
327
|
+
|
|
328
|
+
status = "[green]✓[/green]" if not issues else f"[yellow]⚠ {', '.join(issues)}[/yellow]"
|
|
329
|
+
|
|
330
|
+
cmd_display = tool_def.command[:40] + "..." if len(tool_def.command) > 40 else tool_def.command
|
|
331
|
+
table.add_row(tool_def.name, cmd_display, param_str, status)
|
|
332
|
+
|
|
333
|
+
console.print(table)
|
|
334
|
+
console.print("\n[green]✓[/green] Configuration is valid")
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
console.print(f"[red]✗ Validation failed:[/red] {e}")
|
|
338
|
+
console.print(f"\nConfig file: {config_path}")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@tools_app.command("check")
|
|
343
|
+
def tools_check(
|
|
344
|
+
agent_path: str = typer.Argument(help="Path to agent markdown file"),
|
|
345
|
+
):
|
|
346
|
+
"""Check if an agent's tools are available."""
|
|
347
|
+
from pathlib import Path
|
|
348
|
+
|
|
349
|
+
from tsugite.md_agents import parse_agent_file
|
|
350
|
+
from tsugite.tools import _tools
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
agent_file = Path(agent_path)
|
|
354
|
+
|
|
355
|
+
if not agent_file.exists():
|
|
356
|
+
console.print(f"[red]Agent file not found:[/red] {agent_path}")
|
|
357
|
+
raise typer.Exit(1)
|
|
358
|
+
|
|
359
|
+
# Parse agent
|
|
360
|
+
agent = parse_agent_file(agent_file)
|
|
361
|
+
|
|
362
|
+
console.print(f"Checking tools for: [bold]{agent.config.name}[/bold]\n")
|
|
363
|
+
|
|
364
|
+
if not agent.config.tools:
|
|
365
|
+
console.print("[yellow]No tools configured in agent[/yellow]")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Expand tool specifications
|
|
369
|
+
from tsugite.tools import expand_tool_specs
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
expanded_tools = expand_tool_specs(agent.config.tools)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
console.print(f"[red]Error expanding tool specifications:[/red] {e}")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
|
|
377
|
+
# Check each tool
|
|
378
|
+
available = []
|
|
379
|
+
missing = []
|
|
380
|
+
|
|
381
|
+
for tool_name in expanded_tools:
|
|
382
|
+
if tool_name in _tools:
|
|
383
|
+
available.append(tool_name)
|
|
384
|
+
else:
|
|
385
|
+
missing.append(tool_name)
|
|
386
|
+
|
|
387
|
+
# Display results
|
|
388
|
+
if available:
|
|
389
|
+
console.print(f"[green]✓ Available ({len(available)}):[/green]")
|
|
390
|
+
for tool in available:
|
|
391
|
+
tool_info = _tools[tool]
|
|
392
|
+
console.print(f" • {tool} - {tool_info.description}")
|
|
393
|
+
|
|
394
|
+
if missing:
|
|
395
|
+
console.print(f"\n[red]✗ Missing ({len(missing)}):[/red]")
|
|
396
|
+
for tool in missing:
|
|
397
|
+
console.print(f" • {tool}")
|
|
398
|
+
|
|
399
|
+
console.print("\n[yellow]Suggestions:[/yellow]")
|
|
400
|
+
from tsugite.shell_tool_config import get_custom_tools_config_path
|
|
401
|
+
|
|
402
|
+
config_path = get_custom_tools_config_path()
|
|
403
|
+
if config_path.exists():
|
|
404
|
+
console.print(f" • Check custom tools in: {config_path}")
|
|
405
|
+
else:
|
|
406
|
+
console.print(f" • Create custom tools at: {config_path}")
|
|
407
|
+
|
|
408
|
+
console.print(" • Run [cyan]tsugite tools list[/cyan] to see all tools")
|
|
409
|
+
console.print(" • Run [cyan]tsugite tools add <name> ...[/cyan] to add missing tools")
|
|
410
|
+
|
|
411
|
+
raise typer.Exit(1)
|
|
412
|
+
else:
|
|
413
|
+
console.print("\n[green]✓ All tools are available[/green]")
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
if not isinstance(e, typer.Exit):
|
|
417
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
418
|
+
raise typer.Exit(1)
|
|
419
|
+
raise
|