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 <[email protected]>
This commit is contained in:
rob
2025-11-28 04:45:39 -04:00
co-authored by Claude
parent ba18c0feaa
commit 28dac465e6
7 changed files with 1167 additions and 0 deletions
+343
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()