Implement core SmartTools functionality

- providers.py: Provider abstraction with mock support
- tool.py: Tool loading, saving, and wrapper script generation
- runner.py: Tool execution engine with prompt building
- ui.py: Dialog-based UI for tool management
- cli.py: CLI entry point with list/create/edit/delete/test commands

~430 lines of Python as designed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rob 2025-11-28 04:45:39 -04:00
parent ba18c0feaa
commit 28dac465e6
7 changed files with 1167 additions and 0 deletions

49
pyproject.toml Normal file
View File

@ -0,0 +1,49 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "smarttools"
version = "0.1.0"
description = "A lightweight personal tool builder for AI-powered CLI commands"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Rob"}
]
keywords = ["ai", "cli", "tools", "automation"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
]
dependencies = [
"PyYAML>=6.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
]
[project.scripts]
smarttools = "smarttools.cli:main"
[project.urls]
Homepage = "https://github.com/rob/smarttools"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

View File

@ -0,0 +1,3 @@
"""SmartTools - A lightweight personal tool builder for AI-powered CLI commands."""
__version__ = "0.1.0"

207
src/smarttools/cli.py Normal file
View File

@ -0,0 +1,207 @@
"""CLI entry point for SmartTools."""
import argparse
import sys
from . import __version__
from .tool import list_tools, load_tool, save_tool, delete_tool, Tool, ToolInput
from .ui import run_ui
def cmd_list(args):
"""List all tools."""
tools = list_tools()
if not tools:
print("No tools found.")
print("Create your first tool with: smarttools create <name>")
return 0
print(f"Available tools ({len(tools)}):\n")
for name in tools:
tool = load_tool(name)
if tool:
print(f" {name}")
print(f" {tool.description or 'No description'}")
print(f" Provider: {tool.provider}")
print()
return 0
def cmd_create(args):
"""Create a new tool."""
name = args.name
# Check if already exists
existing = load_tool(name)
if existing and not args.force:
print(f"Error: Tool '{name}' already exists. Use --force to overwrite.")
return 1
# Create minimal tool
tool = Tool(
name=name,
description=args.description or "",
prompt=args.prompt or "Process the following:\n\n{input}",
provider=args.provider or "codex",
provider_args=args.provider_args or "-p",
inputs=[],
post_process=None
)
path = save_tool(tool)
print(f"Created tool '{name}'")
print(f"Config: {path}")
print(f"\nEdit the config to customize, then run: {name} -i <file>")
return 0
def cmd_edit(args):
"""Edit a tool (opens in $EDITOR or nano)."""
import os
import subprocess
tool = load_tool(args.name)
if not tool:
print(f"Error: Tool '{args.name}' not found.")
return 1
from .tool import get_tools_dir
config_path = get_tools_dir() / args.name / "config.yaml"
editor = os.environ.get("EDITOR", "nano")
try:
subprocess.run([editor, str(config_path)], check=True)
print(f"Tool '{args.name}' updated.")
return 0
except subprocess.CalledProcessError:
print(f"Error: Editor failed.")
return 1
except FileNotFoundError:
print(f"Error: Editor '{editor}' not found. Set $EDITOR environment variable.")
return 1
def cmd_delete(args):
"""Delete a tool."""
if not load_tool(args.name):
print(f"Error: Tool '{args.name}' not found.")
return 1
if not args.force:
confirm = input(f"Delete tool '{args.name}'? [y/N] ")
if confirm.lower() != 'y':
print("Cancelled.")
return 0
if delete_tool(args.name):
print(f"Deleted tool '{args.name}'.")
return 0
else:
print(f"Error: Failed to delete '{args.name}'.")
return 1
def cmd_test(args):
"""Test a tool with mock provider."""
tool = load_tool(args.name)
if not tool:
print(f"Error: Tool '{args.name}' not found.")
return 1
from .runner import run_tool
# Read test input
if args.input:
from pathlib import Path
input_text = Path(args.input).read_text()
else:
print("Enter test input (Ctrl+D to end):")
input_text = sys.stdin.read()
print("\n--- Running with mock provider ---\n")
output, code = run_tool(
tool=tool,
input_text=input_text,
custom_args={},
provider_override="mock",
dry_run=args.dry_run,
show_prompt=True,
verbose=True
)
if output:
print("\n--- Output ---\n")
print(output)
return code
def cmd_ui(args):
"""Launch the interactive UI."""
run_ui()
return 0
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
prog="smarttools",
description="A lightweight personal tool builder for AI-powered CLI commands"
)
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# No command = launch UI
# 'list' command
p_list = subparsers.add_parser("list", help="List all tools")
p_list.set_defaults(func=cmd_list)
# 'create' command
p_create = subparsers.add_parser("create", help="Create a new tool")
p_create.add_argument("name", help="Tool name")
p_create.add_argument("-d", "--description", help="Tool description")
p_create.add_argument("-p", "--prompt", help="Prompt template")
p_create.add_argument("--provider", help="AI provider (default: codex)")
p_create.add_argument("--provider-args", help="Provider arguments (default: -p)")
p_create.add_argument("-f", "--force", action="store_true", help="Overwrite existing")
p_create.set_defaults(func=cmd_create)
# 'edit' command
p_edit = subparsers.add_parser("edit", help="Edit a tool config")
p_edit.add_argument("name", help="Tool name")
p_edit.set_defaults(func=cmd_edit)
# 'delete' command
p_delete = subparsers.add_parser("delete", help="Delete a tool")
p_delete.add_argument("name", help="Tool name")
p_delete.add_argument("-f", "--force", action="store_true", help="Skip confirmation")
p_delete.set_defaults(func=cmd_delete)
# 'test' command
p_test = subparsers.add_parser("test", help="Test a tool with mock provider")
p_test.add_argument("name", help="Tool name")
p_test.add_argument("-i", "--input", help="Input file for testing")
p_test.add_argument("--dry-run", action="store_true", help="Show prompt only")
p_test.set_defaults(func=cmd_test)
# 'ui' command (explicit)
p_ui = subparsers.add_parser("ui", help="Launch interactive UI")
p_ui.set_defaults(func=cmd_ui)
args = parser.parse_args()
# If no command, launch UI
if args.command is None:
return cmd_ui(args)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())

110
src/smarttools/providers.py Normal file
View File

@ -0,0 +1,110 @@
"""Provider abstraction for AI CLI tools."""
import subprocess
import shutil
from dataclasses import dataclass
from typing import Optional
@dataclass
class ProviderResult:
"""Result from a provider call."""
text: str
success: bool
error: Optional[str] = None
def call_provider(provider: str, args: str, prompt: str, timeout: int = 300) -> ProviderResult:
"""
Call an AI CLI tool with the given prompt.
Args:
provider: The CLI command (e.g., 'codex', 'claude', 'gemini')
args: Additional arguments for the provider (e.g., '-p')
prompt: The prompt to send
timeout: Maximum execution time in seconds
Returns:
ProviderResult with the response text or error
"""
# Check if provider exists
if not shutil.which(provider):
return ProviderResult(
text="",
success=False,
error=f"Provider '{provider}' not found. Is it installed and in PATH?"
)
# Build command
cmd = f"{provider} {args}".strip()
try:
result = subprocess.run(
cmd,
shell=True,
input=prompt,
capture_output=True,
text=True,
timeout=timeout
)
if result.returncode != 0:
return ProviderResult(
text="",
success=False,
error=f"Provider exited with code {result.returncode}: {result.stderr}"
)
return ProviderResult(text=result.stdout, success=True)
except subprocess.TimeoutExpired:
return ProviderResult(
text="",
success=False,
error=f"Provider timed out after {timeout} seconds"
)
except Exception as e:
return ProviderResult(
text="",
success=False,
error=f"Provider error: {str(e)}"
)
def mock_provider(prompt: str) -> ProviderResult:
"""
Return a mock response for testing.
Args:
prompt: The prompt (used for generating mock response)
Returns:
ProviderResult with mock response
"""
lines = prompt.strip().split('\n')
preview = lines[0][:50] + "..." if len(lines[0]) > 50 else lines[0]
return ProviderResult(
text=f"[MOCK RESPONSE]\n"
f"Prompt length: {len(prompt)} chars, {len(lines)} lines\n"
f"First line: {preview}\n"
f"\n"
f"This is a mock response. Use a real provider for actual output.",
success=True
)
def get_provider_func(provider: str):
"""
Get the appropriate provider function.
Args:
provider: Provider name ('mock' for mock, anything else for real)
Returns:
Callable that takes (prompt) or (provider, args, prompt)
"""
if provider.lower() == "mock":
return lambda prompt, **kwargs: mock_provider(prompt)
else:
return lambda prompt, args="", **kwargs: call_provider(provider, args, prompt, **kwargs)

241
src/smarttools/runner.py Normal file
View File

@ -0,0 +1,241 @@
"""Tool execution engine."""
import argparse
import sys
from pathlib import Path
from typing import Optional
from .tool import load_tool, Tool
from .providers import call_provider, mock_provider, ProviderResult
def build_prompt(tool: Tool, input_text: str, custom_args: dict) -> str:
"""
Build the final prompt by substituting placeholders.
Args:
tool: Tool definition
input_text: Content from input file or stdin
custom_args: Custom argument values
Returns:
Final prompt string
"""
prompt = tool.prompt
# Replace {input} placeholder
prompt = prompt.replace("{input}", input_text)
# Replace custom argument placeholders
for inp in tool.inputs:
value = custom_args.get(inp.name, inp.default)
if value is not None:
prompt = prompt.replace(f"{{{inp.name}}}", str(value))
return prompt
def run_post_process(tool: Tool, output: str) -> str:
"""
Run post-processing script if defined.
Args:
tool: Tool definition
output: Raw output from provider
Returns:
Processed output
"""
if not tool.post_process:
return output
from .tool import get_tools_dir
script_path = get_tools_dir() / tool.name / tool.post_process
if not script_path.exists():
print(f"Warning: Post-process script not found: {script_path}", file=sys.stderr)
return output
# Execute post-process script with output as input
import subprocess
try:
result = subprocess.run(
["python3", str(script_path)],
input=output,
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
return result.stdout
else:
print(f"Warning: Post-process failed: {result.stderr}", file=sys.stderr)
return output
except Exception as e:
print(f"Warning: Post-process error: {e}", file=sys.stderr)
return output
def run_tool(
tool: Tool,
input_text: str,
custom_args: dict,
provider_override: Optional[str] = None,
dry_run: bool = False,
show_prompt: bool = False,
verbose: bool = False
) -> tuple[str, int]:
"""
Execute a tool.
Args:
tool: Tool definition
input_text: Input content
custom_args: Custom argument values
provider_override: Override the tool's provider
dry_run: Just show prompt, don't call provider
show_prompt: Show prompt in addition to output
verbose: Show debug info
Returns:
Tuple of (output_text, exit_code)
"""
# Build prompt
prompt = build_prompt(tool, input_text, custom_args)
if verbose:
print(f"[verbose] Tool: {tool.name}", file=sys.stderr)
print(f"[verbose] Provider: {provider_override or tool.provider}", file=sys.stderr)
print(f"[verbose] Prompt length: {len(prompt)} chars", file=sys.stderr)
# Dry run - just show prompt
if dry_run:
return prompt, 0
# Show prompt if requested
if show_prompt:
print("=== PROMPT ===", file=sys.stderr)
print(prompt, file=sys.stderr)
print("=== END PROMPT ===", file=sys.stderr)
# Call provider
provider = provider_override or tool.provider
if provider.lower() == "mock":
result = mock_provider(prompt)
else:
result = call_provider(provider, tool.provider_args, prompt)
if not result.success:
print(f"Error: {result.error}", file=sys.stderr)
return "", 2 # Exit code 2 for provider errors
output = result.text
# Post-process if defined
output = run_post_process(tool, output)
return output, 0
def create_argument_parser(tool: Tool) -> argparse.ArgumentParser:
"""
Create an argument parser for a tool.
Args:
tool: Tool definition
Returns:
Configured ArgumentParser
"""
parser = argparse.ArgumentParser(
prog=tool.name,
description=tool.description or f"SmartTools: {tool.name}"
)
# Universal flags
parser.add_argument("-i", "--input", dest="input_file",
help="Input file (reads from stdin if omitted)")
parser.add_argument("-o", "--output", dest="output_file",
help="Output file (writes to stdout if omitted)")
parser.add_argument("--dry-run", action="store_true",
help="Show prompt without calling AI provider")
parser.add_argument("--show-prompt", action="store_true",
help="Show prompt in addition to output")
parser.add_argument("-p", "--provider",
help="Override provider (e.g., --provider mock)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Show debug information")
# Tool-specific flags
for inp in tool.inputs:
parser.add_argument(
inp.flag,
dest=inp.name,
default=inp.default,
help=inp.description or f"{inp.name} (default: {inp.default})"
)
return parser
def main():
"""Entry point for tool execution via wrapper script."""
if len(sys.argv) < 2:
print("Usage: python -m smarttools.runner <tool_name> [args...]", file=sys.stderr)
sys.exit(1)
tool_name = sys.argv[1]
tool = load_tool(tool_name)
if tool is None:
print(f"Error: Tool '{tool_name}' not found", file=sys.stderr)
sys.exit(1)
# Parse remaining arguments
parser = create_argument_parser(tool)
args = parser.parse_args(sys.argv[2:])
# Read input
if args.input_file:
input_path = Path(args.input_file)
if not input_path.exists():
print(f"Error: Input file not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
input_text = input_path.read_text()
else:
# Read from stdin
if sys.stdin.isatty():
print("Reading from stdin (Ctrl+D to end):", file=sys.stderr)
input_text = sys.stdin.read()
# Collect custom args
custom_args = {}
for inp in tool.inputs:
value = getattr(args, inp.name, None)
if value is not None:
custom_args[inp.name] = value
# Run tool
output, exit_code = run_tool(
tool=tool,
input_text=input_text,
custom_args=custom_args,
provider_override=args.provider,
dry_run=args.dry_run,
show_prompt=args.show_prompt,
verbose=args.verbose
)
# Write output
if exit_code == 0 and output:
if args.output_file:
Path(args.output_file).write_text(output)
else:
print(output)
sys.exit(exit_code)
if __name__ == "__main__":
main()

214
src/smarttools/tool.py Normal file
View File

@ -0,0 +1,214 @@
"""Tool loading, saving, and management."""
import os
import stat
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import yaml
# Default tools directory
TOOLS_DIR = Path.home() / ".smarttools"
# Default bin directory for wrapper scripts
BIN_DIR = Path.home() / ".local" / "bin"
@dataclass
class ToolInput:
"""Definition of a custom input argument."""
name: str
flag: str
default: Optional[str] = None
description: str = ""
@dataclass
class Tool:
"""A SmartTools tool definition."""
name: str
description: str
prompt: str
provider: str
provider_args: str = ""
inputs: list[ToolInput] = field(default_factory=list)
post_process: Optional[str] = None
@classmethod
def from_dict(cls, data: dict) -> "Tool":
"""Create a Tool from a dictionary (parsed YAML)."""
inputs = []
for inp in data.get("inputs", []):
inputs.append(ToolInput(
name=inp["name"],
flag=inp.get("flag", f"--{inp['name']}"),
default=inp.get("default"),
description=inp.get("description", "")
))
return cls(
name=data["name"],
description=data.get("description", ""),
prompt=data["prompt"],
provider=data["provider"],
provider_args=data.get("provider_args", ""),
inputs=inputs,
post_process=data.get("post_process")
)
def to_dict(self) -> dict:
"""Convert Tool to dictionary for YAML serialization."""
data = {
"name": self.name,
"description": self.description,
"prompt": self.prompt,
"provider": self.provider,
}
if self.provider_args:
data["provider_args"] = self.provider_args
if self.inputs:
data["inputs"] = [
{
"name": inp.name,
"flag": inp.flag,
"default": inp.default,
"description": inp.description,
}
for inp in self.inputs
]
if self.post_process:
data["post_process"] = self.post_process
return data
def get_tools_dir() -> Path:
"""Get the tools directory, creating it if needed."""
TOOLS_DIR.mkdir(parents=True, exist_ok=True)
return TOOLS_DIR
def get_bin_dir() -> Path:
"""Get the bin directory for wrapper scripts, creating it if needed."""
BIN_DIR.mkdir(parents=True, exist_ok=True)
return BIN_DIR
def list_tools() -> list[str]:
"""List all available tools."""
tools_dir = get_tools_dir()
tools = []
for item in tools_dir.iterdir():
if item.is_dir():
config = item / "config.yaml"
if config.exists():
tools.append(item.name)
return sorted(tools)
def load_tool(name: str) -> Optional[Tool]:
"""
Load a tool by name.
Args:
name: Tool name
Returns:
Tool object or None if not found
"""
config_path = get_tools_dir() / name / "config.yaml"
if not config_path.exists():
return None
try:
data = yaml.safe_load(config_path.read_text())
return Tool.from_dict(data)
except Exception:
return None
def save_tool(tool: Tool) -> Path:
"""
Save a tool to disk.
Args:
tool: Tool object to save
Returns:
Path to the saved config file
"""
tool_dir = get_tools_dir() / tool.name
tool_dir.mkdir(parents=True, exist_ok=True)
config_path = tool_dir / "config.yaml"
config_path.write_text(yaml.dump(tool.to_dict(), default_flow_style=False, sort_keys=False))
# Create wrapper script
create_wrapper_script(tool.name)
return config_path
def delete_tool(name: str) -> bool:
"""
Delete a tool.
Args:
name: Tool name
Returns:
True if deleted, False if not found
"""
tool_dir = get_tools_dir() / name
if not tool_dir.exists():
return False
# Remove wrapper script
wrapper = get_bin_dir() / name
if wrapper.exists():
wrapper.unlink()
# Remove tool directory
import shutil
shutil.rmtree(tool_dir)
return True
def create_wrapper_script(name: str) -> Path:
"""
Create a wrapper script for a tool in ~/.local/bin.
Args:
name: Tool name
Returns:
Path to the wrapper script
"""
bin_dir = get_bin_dir()
wrapper_path = bin_dir / name
script = f"""#!/bin/bash
# SmartTools wrapper for '{name}'
# Auto-generated - do not edit
exec python3 -m smarttools.runner {name} "$@"
"""
wrapper_path.write_text(script)
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
return wrapper_path
def tool_exists(name: str) -> bool:
"""Check if a tool exists."""
return (get_tools_dir() / name / "config.yaml").exists()

343
src/smarttools/ui.py Normal file
View File

@ -0,0 +1,343 @@
"""Dialog-based UI for managing SmartTools."""
import subprocess
import sys
from typing import Optional, Tuple
from .tool import Tool, ToolInput, list_tools, load_tool, save_tool, delete_tool, tool_exists
def check_dialog() -> str:
"""Check for available dialog program. Returns 'dialog', 'whiptail', or None."""
for prog in ["dialog", "whiptail"]:
try:
subprocess.run([prog, "--version"], capture_output=True, check=False)
return prog
except FileNotFoundError:
continue
return None
def run_dialog(args: list[str], dialog_prog: str = "dialog") -> Tuple[int, str]:
"""
Run a dialog command and return (exit_code, output).
Args:
args: Dialog arguments
dialog_prog: Dialog program to use
Returns:
Tuple of (exit_code, captured_output)
"""
cmd = [dialog_prog] + args
result = subprocess.run(cmd, capture_output=True, text=True)
# dialog outputs to stderr
return result.returncode, result.stderr.strip()
def show_menu(title: str, choices: list[tuple[str, str]], dialog_prog: str) -> Optional[str]:
"""
Show a menu and return the selected item.
Args:
title: Menu title
choices: List of (tag, description) tuples
dialog_prog: Dialog program to use
Returns:
Selected tag or None if cancelled
"""
args = ["--title", title, "--menu", "", "15", "60", str(len(choices))]
for tag, desc in choices:
args.extend([tag, desc])
code, output = run_dialog(args, dialog_prog)
return output if code == 0 else None
def show_input(title: str, prompt: str, initial: str = "", dialog_prog: str = "dialog") -> Optional[str]:
"""
Show an input box and return the entered text.
Args:
title: Dialog title
prompt: Prompt text
initial: Initial value
dialog_prog: Dialog program to use
Returns:
Entered text or None if cancelled
"""
args = ["--title", title, "--inputbox", prompt, "10", "60", initial]
code, output = run_dialog(args, dialog_prog)
return output if code == 0 else None
def show_textbox(title: str, text: str, dialog_prog: str = "dialog") -> Optional[str]:
"""
Show a text editor for multi-line input.
Args:
title: Dialog title
text: Initial text
dialog_prog: Dialog program to use
Returns:
Edited text or None if cancelled
"""
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
f.write(text)
temp_path = f.name
args = ["--title", title, "--editbox", temp_path, "20", "70"]
code, output = run_dialog(args, dialog_prog)
import os
os.unlink(temp_path)
return output if code == 0 else None
def show_yesno(title: str, text: str, dialog_prog: str = "dialog") -> bool:
"""
Show a yes/no dialog.
Args:
title: Dialog title
text: Question text
dialog_prog: Dialog program to use
Returns:
True if yes, False if no
"""
args = ["--title", title, "--yesno", text, "10", "60"]
code, _ = run_dialog(args, dialog_prog)
return code == 0
def show_message(title: str, text: str, dialog_prog: str = "dialog"):
"""Show a message box."""
args = ["--title", title, "--msgbox", text, "10", "60"]
run_dialog(args, dialog_prog)
def create_tool_form(dialog_prog: str, existing: Optional[Tool] = None) -> Optional[Tool]:
"""
Show form for creating/editing a tool.
Args:
dialog_prog: Dialog program to use
existing: Existing tool to edit (None for new)
Returns:
Tool object or None if cancelled
"""
# Get basic info
name = show_input(
"Tool Name",
"Enter tool name (used as command):",
existing.name if existing else "",
dialog_prog
)
if not name:
return None
# Check if name already exists (for new tools)
if not existing and tool_exists(name):
if not show_yesno("Overwrite?", f"Tool '{name}' already exists. Overwrite?", dialog_prog):
return None
description = show_input(
"Description",
"Enter tool description:",
existing.description if existing else "",
dialog_prog
)
if description is None:
return None
provider = show_input(
"Provider",
"Enter AI provider command (e.g., codex, claude, gemini):",
existing.provider if existing else "codex",
dialog_prog
)
if not provider:
return None
provider_args = show_input(
"Provider Args",
"Enter provider arguments (e.g., -p):",
existing.provider_args if existing else "-p",
dialog_prog
)
if provider_args is None:
return None
# Edit prompt
default_prompt = existing.prompt if existing else "Process the following input:\n\n{input}"
prompt = show_textbox("Prompt Template", default_prompt, dialog_prog)
if not prompt:
return None
# For simplicity, skip custom inputs in the UI for now
# They can be added by editing config.yaml directly
inputs = existing.inputs if existing else []
return Tool(
name=name,
description=description,
prompt=prompt,
provider=provider,
provider_args=provider_args,
inputs=inputs,
post_process=existing.post_process if existing else None
)
def ui_list_tools(dialog_prog: str):
"""Show list of tools."""
tools = list_tools()
if not tools:
show_message("Tools", "No tools found.\n\nCreate your first tool from the main menu.", dialog_prog)
return
text = "Available tools:\n\n"
for name in tools:
tool = load_tool(name)
if tool:
text += f" {name}: {tool.description or 'No description'}\n"
text += f" Provider: {tool.provider}\n\n"
show_message("Tools", text, dialog_prog)
def ui_create_tool(dialog_prog: str):
"""Create a new tool."""
tool = create_tool_form(dialog_prog)
if tool:
path = save_tool(tool)
show_message("Success", f"Tool '{tool.name}' created!\n\nConfig: {path}\n\nRun it with: {tool.name} -i <file>", dialog_prog)
def ui_edit_tool(dialog_prog: str):
"""Edit an existing tool."""
tools = list_tools()
if not tools:
show_message("Edit Tool", "No tools found.", dialog_prog)
return
choices = [(name, load_tool(name).description or "No description") for name in tools]
selected = show_menu("Select Tool to Edit", choices, dialog_prog)
if selected:
existing = load_tool(selected)
if existing:
tool = create_tool_form(dialog_prog, existing)
if tool:
save_tool(tool)
show_message("Success", f"Tool '{tool.name}' updated!", dialog_prog)
def ui_delete_tool(dialog_prog: str):
"""Delete a tool."""
tools = list_tools()
if not tools:
show_message("Delete Tool", "No tools found.", dialog_prog)
return
choices = [(name, load_tool(name).description or "No description") for name in tools]
selected = show_menu("Select Tool to Delete", choices, dialog_prog)
if selected:
if show_yesno("Confirm Delete", f"Delete tool '{selected}'?\n\nThis cannot be undone.", dialog_prog):
if delete_tool(selected):
show_message("Deleted", f"Tool '{selected}' deleted.", dialog_prog)
else:
show_message("Error", f"Failed to delete '{selected}'.", dialog_prog)
def ui_test_tool(dialog_prog: str):
"""Test a tool with mock provider."""
tools = list_tools()
if not tools:
show_message("Test Tool", "No tools found.", dialog_prog)
return
choices = [(name, load_tool(name).description or "No description") for name in tools]
selected = show_menu("Select Tool to Test", choices, dialog_prog)
if selected:
tool = load_tool(selected)
if tool:
# Get test input
test_input = show_textbox("Test Input", "Enter test input here...", dialog_prog)
if test_input:
from .runner import run_tool
output, code = run_tool(
tool=tool,
input_text=test_input,
custom_args={},
provider_override="mock",
dry_run=False,
show_prompt=False,
verbose=False
)
show_message(
"Test Result",
f"Exit code: {code}\n\n--- Output ---\n{output[:500]}",
dialog_prog
)
def main_menu(dialog_prog: str):
"""Show the main menu."""
while True:
choice = show_menu(
"SmartTools Manager",
[
("list", "List all tools"),
("create", "Create new tool"),
("edit", "Edit existing tool"),
("delete", "Delete tool"),
("test", "Test tool (mock provider)"),
("exit", "Exit"),
],
dialog_prog
)
if choice is None or choice == "exit":
break
elif choice == "list":
ui_list_tools(dialog_prog)
elif choice == "create":
ui_create_tool(dialog_prog)
elif choice == "edit":
ui_edit_tool(dialog_prog)
elif choice == "delete":
ui_delete_tool(dialog_prog)
elif choice == "test":
ui_test_tool(dialog_prog)
def run_ui():
"""Entry point for the UI."""
dialog_prog = check_dialog()
if not dialog_prog:
print("Error: Neither 'dialog' nor 'whiptail' found.", file=sys.stderr)
print("Install with: sudo apt install dialog", file=sys.stderr)
sys.exit(1)
try:
main_menu(dialog_prog)
except KeyboardInterrupt:
pass
finally:
# Clear screen on exit
subprocess.run(["clear"], check=False)
if __name__ == "__main__":
run_ui()