From 23723eaddbf5dbbc4e2c2fa512078eaa604a3076 Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 20 Jul 2026 17:11:21 -0300 Subject: [PATCH] Add cmdforge list --json, --filter, and run-once for LLM-friendly one-shot execution --- src/cmdforge/cli/__init__.py | 12 +++- src/cmdforge/cli/tool_commands.py | 98 +++++++++++++++++++++++-------- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/src/cmdforge/cli/__init__.py b/src/cmdforge/cli/__init__.py index 6194e87..e3cfc24 100644 --- a/src/cmdforge/cli/__init__.py +++ b/src/cmdforge/cli/__init__.py @@ -6,7 +6,7 @@ import sys from .. import __version__ from .tool_commands import ( - cmd_list, cmd_create, cmd_edit, cmd_delete, cmd_test, cmd_run, + cmd_list, cmd_create, cmd_edit, cmd_delete, cmd_test, cmd_run, cmd_run_once, cmd_ui, cmd_refresh, cmd_docs, cmd_check ) from .provider_commands import cmd_providers @@ -33,6 +33,8 @@ def main(): # No command = launch UI # 'list' command p_list = subparsers.add_parser("list", help="List all tools") + p_list.add_argument("--json", action="store_true", help="Output as JSON") + p_list.add_argument("--filter", help="Filter by name or description (substring match)") p_list.set_defaults(func=cmd_list) # 'create' command @@ -77,6 +79,14 @@ def main(): p_run.add_argument("tool_args", nargs=argparse.REMAINDER, help="Additional tool-specific arguments (use -- to separate)") p_run.set_defaults(func=cmd_run) + # 'run-once' command — one-shot without creating a tool + p_run_once = subparsers.add_parser("run-once", help="Run a one-shot prompt through a provider without creating a tool") + p_run_once.add_argument("prompt", help="Prompt text (or '-' to read from stdin)") + p_run_once.add_argument("-p", "--provider", default="opencode-pickle", help="Provider to use") + p_run_once.add_argument("--max-tokens", type=int, help="Max output tokens") + p_run_once.add_argument("--timeout", type=int, default=60, help="Timeout in seconds") + p_run_once.set_defaults(func=cmd_run_once) + # 'ui' command (explicit) p_ui = subparsers.add_parser("ui", help="Launch interactive UI") p_ui.set_defaults(func=cmd_ui) diff --git a/src/cmdforge/cli/tool_commands.py b/src/cmdforge/cli/tool_commands.py index 74c7386..11a9b8d 100644 --- a/src/cmdforge/cli/tool_commands.py +++ b/src/cmdforge/cli/tool_commands.py @@ -12,8 +12,26 @@ from ..gui import run_gui def cmd_list(args): """List all tools.""" + import json + import sys + tools = list_tools() + filter_text = getattr(args, "filter", "") or "" + + if getattr(args, "json", False): + result = [] + for name in sorted(tools): + tool = load_tool(name) + if not tool: + continue + if filter_text and filter_text.lower() not in name.lower() and filter_text.lower() not in (tool.description or "").lower(): + continue + result.append(tool.to_dict()) + json.dump(result, sys.stdout, indent=2) + print() + return 0 + if not tools: print("No tools found.") print("Create your first tool with: cmdforge ui") @@ -22,36 +40,66 @@ def cmd_list(args): print(f"Available tools ({len(tools)}):\n") for name in tools: tool = load_tool(name) - if tool: - # Show source indicator for imported tools - source_marker = "" - if tool.source and tool.source.type == "imported": - source_marker = " [imported]" - elif tool.source and tool.source.type == "forked": - source_marker = " [forked]" + if not tool: + continue + if filter_text and filter_text.lower() not in name.lower() and filter_text.lower() not in (tool.description or "").lower(): + continue - print(f" {name}{source_marker}") - print(f" {tool.description or 'No description'}") + # Show source indicator for imported tools + source_marker = "" + if tool.source and tool.source.type == "imported": + source_marker = " [imported]" + elif tool.source and tool.source.type == "forked": + source_marker = " [forked]" - # Show arguments - if tool.arguments: - args_str = ", ".join(arg.flag for arg in tool.arguments) - print(f" Arguments: {args_str}") + print(f" {name}{source_marker}") + print(f" {tool.description or 'No description'}") - # Show steps - if tool.steps: - step_info = [] - for step in tool.steps: - if isinstance(step, PromptStep): - step_info.append(f"PROMPT[{step.provider}]") - elif isinstance(step, CodeStep): - step_info.append("CODE") - elif isinstance(step, ToolStep): - step_info.append(f"TOOL[{step.tool}]") - print(f" Steps: {' -> '.join(step_info)}") + # Show arguments + if tool.arguments: + args_str = ", ".join(arg.flag for arg in tool.arguments) + print(f" Arguments: {args_str}") - print() + # Show steps + if tool.steps: + step_info = [] + for step in tool.steps: + if isinstance(step, PromptStep): + step_info.append(f"PROMPT[{step.provider}]") + elif isinstance(step, CodeStep): + step_info.append("CODE") + elif isinstance(step, ToolStep): + step_info.append(f"TOOL[{step.tool}]") + print(f" Steps: {' -> '.join(step_info)}") + print() + + return 0 + + +def cmd_run_once(args): + """Run a one-shot prompt through a provider without creating a tool.""" + import sys + from ..providers import call_provider + + prompt = args.prompt + if prompt == "-": + prompt = sys.stdin.read() + + if not prompt.strip(): + print("Error: No prompt provided.", file=sys.stderr) + return 1 + + provider = args.provider or "opencode-pickle" + + print(f"[run-once] Provider: {provider}", file=sys.stderr) + result = call_provider(provider, prompt, timeout=args.timeout, max_tokens=args.max_tokens) + + if not result.success: + print(f"Error: {result.error}", file=sys.stderr) + return 1 + + print(result.text) return 0