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.
Files changed (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. 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