swarm_cli 2.0.0.pre.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 796c9d34130105fe9e4a2d6896fa01e5ec73222f7c334e430b5c2d736055905e
4
+ data.tar.gz: a9abe83f5e316339e27ea9d2ca52fcffbb29d0c0efcc20dd98ba6131cf2ee2e7
5
+ SHA512:
6
+ metadata.gz: 87f901492c2bc58b85b64e4a06e925eed25629e69b4732acbb07056211cb2920f65fd4c79f8305720c4cd62bac49dd20436e16864dd713ba89c6f9b922aae1f2
7
+ data.tar.gz: 0b7f8c5736f63f5c427a60a530f88f32939c654d2a2295313152b70235e95d06f3288d3ede7576b5f9f6e13f7cf2c4e12340d0d2cbf83d278c81e6f927484983
data/exe/swarm ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "swarm_cli"
5
+
6
+ SwarmCLI::CLI.start(ARGV)
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ class CLI
5
+ class << self
6
+ def start(args)
7
+ SwarmSDK.refresh_models_silently
8
+ new(args).run
9
+ end
10
+ end
11
+
12
+ def initialize(args)
13
+ @args = args
14
+ end
15
+
16
+ def run
17
+ # Handle special cases first
18
+ if @args.empty? || @args.include?("--help") || @args.include?("-h")
19
+ print_help
20
+ exit(0)
21
+ end
22
+
23
+ if @args.include?("--version") || @args.include?("-v")
24
+ print_version
25
+ exit(0)
26
+ end
27
+
28
+ # Extract command
29
+ command = @args.first
30
+
31
+ # Route to command
32
+ case command
33
+ when "run"
34
+ run_command(@args[1..])
35
+ when "mcp"
36
+ mcp_command(@args[1..])
37
+ else
38
+ $stderr.puts "Unknown command: #{command}"
39
+ $stderr.puts
40
+ print_help
41
+ exit(1)
42
+ end
43
+ rescue StandardError => e
44
+ $stderr.puts "Fatal error: #{e.message}"
45
+ exit(1)
46
+ end
47
+
48
+ private
49
+
50
+ def mcp_command(args)
51
+ # MCP has subcommands
52
+ subcommand = args.first
53
+
54
+ case subcommand
55
+ when "serve"
56
+ mcp_serve_command(args[1..])
57
+ when "tools"
58
+ mcp_tools_command(args[1..])
59
+ else
60
+ $stderr.puts "Unknown mcp subcommand: #{subcommand}"
61
+ $stderr.puts
62
+ $stderr.puts "Available mcp subcommands:"
63
+ $stderr.puts " serve Start an MCP server exposing swarm lead agent"
64
+ $stderr.puts " tools Start an MCP server exposing SwarmSDK tools"
65
+ exit(1)
66
+ end
67
+ end
68
+
69
+ def mcp_serve_command(args)
70
+ # Parse options
71
+ options = McpServeOptions.new
72
+ options.parse(args)
73
+
74
+ # Execute mcp serve command
75
+ Commands::McpServe.new(options).execute
76
+ rescue TTY::Option::InvalidParameter, TTY::Option::InvalidArgument => e
77
+ $stderr.puts "Error: #{e.message}"
78
+ $stderr.puts
79
+ $stderr.puts options.help
80
+ exit(1)
81
+ end
82
+
83
+ def mcp_tools_command(args)
84
+ # Parse options
85
+ options = McpToolsOptions.new
86
+ options.parse(args)
87
+
88
+ # Execute mcp tools command
89
+ Commands::McpTools.new(options).execute
90
+ rescue TTY::Option::InvalidParameter, TTY::Option::InvalidArgument => e
91
+ $stderr.puts "Error: #{e.message}"
92
+ $stderr.puts
93
+ $stderr.puts options.help
94
+ exit(1)
95
+ end
96
+
97
+ def run_command(args)
98
+ # Parse options
99
+ options = Options.new
100
+ options.parse(args)
101
+
102
+ # Execute run command
103
+ Commands::Run.new(options).execute
104
+ rescue TTY::Option::InvalidParameter, TTY::Option::InvalidArgument => e
105
+ $stderr.puts "Error: #{e.message}"
106
+ $stderr.puts
107
+ $stderr.puts options.help
108
+ exit(1)
109
+ end
110
+
111
+ def print_help
112
+ puts
113
+ puts "SwarmCLI v#{VERSION} - AI Agent Orchestration"
114
+ puts
115
+ puts "Usage:"
116
+ puts " swarm run CONFIG_FILE -p PROMPT [options]"
117
+ puts " swarm mcp serve CONFIG_FILE"
118
+ puts " swarm mcp tools [TOOL_NAMES...]"
119
+ puts
120
+ puts "Commands:"
121
+ puts " run Execute a swarm with AI agents"
122
+ puts " mcp serve Start an MCP server exposing swarm lead agent"
123
+ puts " mcp tools Start an MCP server exposing SwarmSDK tools"
124
+ puts
125
+ puts "Options:"
126
+ puts " -p, --prompt PROMPT Task prompt for the swarm"
127
+ puts " --output-format FORMAT Output format: 'human' or 'json' (default: human)"
128
+ puts " -q, --quiet Suppress progress output (human format only)"
129
+ puts " --truncate Truncate long outputs for concise view"
130
+ puts " --verbose Show system reminders and additional debug information"
131
+ puts " -h, --help Print help"
132
+ puts " -v, --version Print version"
133
+ puts
134
+ puts "Examples:"
135
+ puts " swarm run team.yml -p 'Build a REST API'"
136
+ puts " echo 'Build a REST API' | swarm run team.yml"
137
+ puts " swarm run team.yml -p 'Refactor code' --output-format json"
138
+ puts " swarm mcp serve team.yml"
139
+ puts " swarm mcp tools # Expose all SwarmSDK tools"
140
+ puts " swarm mcp tools Bash Grep Read # Space-separated tools"
141
+ puts " swarm mcp tools ScratchpadWrite,ScratchpadRead # Comma-separated tools"
142
+ puts
143
+ end
144
+
145
+ def print_version
146
+ puts "SwarmCLI v#{VERSION}"
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module Commands
5
+ # McpServe command starts an MCP server that exposes the swarm's lead agent as a tool.
6
+ #
7
+ # Usage:
8
+ # swarm mcp serve config.yml
9
+ #
10
+ # The server uses stdio transport and exposes a "swarm" tool that executes tasks
11
+ # through the configured lead agent.
12
+ class McpServe
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def execute
20
+ # Validate options
21
+ options.validate!
22
+
23
+ # Load swarm configuration to validate it
24
+ config_path = options.config_file
25
+ unless File.exist?(config_path)
26
+ $stderr.puts "Error: Configuration file not found: #{config_path}"
27
+ exit(1)
28
+ end
29
+
30
+ # Validate the swarm configuration
31
+ begin
32
+ SwarmSDK::Swarm.load(config_path)
33
+ rescue SwarmSDK::ConfigurationError => e
34
+ $stderr.puts "Error: Invalid swarm configuration: #{e.message}"
35
+ exit(1)
36
+ end
37
+
38
+ # MCP servers should be quiet - stdout is reserved for MCP protocol
39
+ # Errors will still be logged to stderr
40
+
41
+ # Start the MCP server
42
+ start_mcp_server(config_path)
43
+ rescue Interrupt
44
+ # User cancelled (Ctrl+C) - silent exit
45
+ exit(130)
46
+ rescue StandardError => e
47
+ # Unexpected errors - always log to stderr
48
+ $stderr.puts "Fatal error: #{e.message}"
49
+ $stderr.puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
50
+ exit(1)
51
+ end
52
+
53
+ private
54
+
55
+ def start_mcp_server(config_path)
56
+ require "fast_mcp"
57
+
58
+ # Create the server
59
+ server = FastMcp::Server.new(
60
+ name: "swarm-mcp-server",
61
+ version: SwarmCLI::VERSION,
62
+ )
63
+
64
+ # Register the swarm tool
65
+ tool_class = create_swarm_tool_class(config_path)
66
+ server.register_tool(tool_class)
67
+
68
+ # Start with stdio transport (default)
69
+ server.start
70
+ end
71
+
72
+ def create_swarm_tool_class(config_path)
73
+ # Create a tool class dynamically with the config path bound
74
+ Class.new(FastMcp::Tool) do
75
+ # Explicit tool name required for anonymous classes
76
+ tool_name "task"
77
+
78
+ description "Execute tasks through the SwarmSDK lead agent"
79
+
80
+ arguments do
81
+ required(:task).filled(:string).description("The task or prompt to execute")
82
+ optional(:description).filled(:string).description("Brief description of the task")
83
+ optional(:thinking_budget).filled(:string, included_in?: ["think", "think hard", "think harder", "ultrathink"]).description("Thinking budget level")
84
+ end
85
+
86
+ # Store config path as class variable
87
+ @config_path = config_path
88
+
89
+ class << self
90
+ attr_accessor :config_path
91
+ end
92
+
93
+ define_method(:call) do |task:, description: nil, thinking_budget: nil|
94
+ # Load swarm for each execution (ensures fresh state)
95
+ swarm = SwarmSDK::Swarm.load(self.class.config_path)
96
+
97
+ # Build prompt with thinking budget if provided
98
+ prompt = task
99
+ if thinking_budget
100
+ prompt = "<thinking_budget>#{thinking_budget}</thinking_budget>\n\n#{task}"
101
+ end
102
+
103
+ # Execute the task (description is metadata only, not passed to execute)
104
+ result = swarm.execute(prompt)
105
+
106
+ # Check for errors
107
+ if result.failure?
108
+ {
109
+ success: false,
110
+ error: result.error.message,
111
+ task: task,
112
+ description: description,
113
+ }
114
+ else
115
+ # On success, return just the content string
116
+ result.content
117
+ end
118
+ rescue StandardError => e
119
+ {
120
+ success: false,
121
+ error: e.message,
122
+ task: task,
123
+ description: description,
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module Commands
5
+ # McpTools command starts an MCP server that exposes SwarmSDK tools.
6
+ #
7
+ # Usage:
8
+ # swarm mcp tools # Expose all available tools
9
+ # swarm mcp tools Bash Grep # Expose only Bash and Grep
10
+ #
11
+ # The server uses stdio transport and exposes SwarmSDK tools as MCP tools.
12
+ class McpTools
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ @scratchpad = SwarmSDK::Scratchpad.new
18
+ end
19
+
20
+ def execute
21
+ # Validate options
22
+ options.validate!
23
+
24
+ # Determine which tools to expose
25
+ tools_to_expose = determine_tools
26
+
27
+ if tools_to_expose.empty?
28
+ $stderr.puts "Error: No tools available to expose"
29
+ exit(1)
30
+ end
31
+
32
+ # Start the MCP server
33
+ start_mcp_server(tools_to_expose)
34
+ rescue Interrupt
35
+ # User cancelled (Ctrl+C) - silent exit
36
+ exit(130)
37
+ rescue StandardError => e
38
+ # Unexpected errors - always log to stderr
39
+ $stderr.puts "Fatal error: #{e.message}"
40
+ $stderr.puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
41
+ exit(1)
42
+ end
43
+
44
+ private
45
+
46
+ def determine_tools
47
+ if options.tool_names.any?
48
+ # Use specified tools
49
+ options.tool_names.map(&:to_sym)
50
+ else
51
+ # Default: expose all available tools
52
+ SwarmSDK::Tools::Registry.available_names
53
+ end
54
+ end
55
+
56
+ def start_mcp_server(tool_names)
57
+ require "fast_mcp"
58
+
59
+ # Create the server
60
+ server = FastMcp::Server.new(
61
+ name: "swarm-tools-server",
62
+ version: SwarmCLI::VERSION,
63
+ )
64
+
65
+ # Register each tool
66
+ tool_names.each do |tool_name|
67
+ tool_class = create_mcp_tool_wrapper(tool_name)
68
+ server.register_tool(tool_class)
69
+ end
70
+
71
+ # Start with stdio transport (default)
72
+ server.start
73
+ end
74
+
75
+ def create_special_tool_instance(tool_name)
76
+ case tool_name
77
+ when :Read
78
+ SwarmSDK::Tools::Read.create_for_agent(:mcp)
79
+ when :Write
80
+ SwarmSDK::Tools::Write.create_for_agent(:mcp)
81
+ when :Edit
82
+ SwarmSDK::Tools::Edit.create_for_agent(:mcp)
83
+ when :MultiEdit
84
+ SwarmSDK::Tools::MultiEdit.create_for_agent(:mcp)
85
+ when :TodoWrite
86
+ SwarmSDK::Tools::TodoWrite.create_for_agent(:mcp)
87
+ when :ScratchpadWrite
88
+ SwarmSDK::Tools::ScratchpadWrite.create_for_scratchpad(@scratchpad)
89
+ when :ScratchpadRead
90
+ SwarmSDK::Tools::ScratchpadRead.create_for_scratchpad(@scratchpad)
91
+ when :ScratchpadList
92
+ SwarmSDK::Tools::ScratchpadList.create_for_scratchpad(@scratchpad)
93
+ else
94
+ raise "Unknown special tool: #{tool_name}"
95
+ end
96
+ end
97
+
98
+ def create_mcp_tool_wrapper(tool_name)
99
+ sdk_tool_class_or_special = SwarmSDK::Tools::Registry.get(tool_name)
100
+
101
+ # Get the actual tool instance for special tools
102
+ sdk_tool = if sdk_tool_class_or_special == :special
103
+ create_special_tool_instance(tool_name)
104
+ else
105
+ sdk_tool_class_or_special.new
106
+ end
107
+
108
+ # Get tool metadata
109
+ tool_description = sdk_tool.respond_to?(:description) ? sdk_tool.description : "SwarmSDK #{tool_name} tool"
110
+ tool_params = sdk_tool.class.respond_to?(:parameters) ? sdk_tool.class.parameters : {}
111
+
112
+ # Create an MCP tool wrapper
113
+ Class.new(FastMcp::Tool) do
114
+ tool_name tool_name.to_s
115
+ description tool_description
116
+
117
+ # Map RubyLLM parameters to fast-mcp arguments
118
+ arguments do
119
+ tool_params.each do |param_name, param_obj|
120
+ param_type = param_obj.type == "integer" ? :integer : :string
121
+ if param_obj.required
122
+ required(param_name).filled(param_type).description(param_obj.description || "")
123
+ else
124
+ optional(param_name).filled(param_type).description(param_obj.description || "")
125
+ end
126
+ end
127
+ end
128
+
129
+ # Capture sdk_tool in closure
130
+ define_method(:call) do |**kwargs|
131
+ result = sdk_tool.execute(**kwargs)
132
+
133
+ # Return string output for MCP
134
+ if result.is_a?(Hash)
135
+ result[:output] || result[:content] || result[:files]&.join("\n") || result.to_s
136
+ else
137
+ result.to_s
138
+ end
139
+ rescue StandardError => e
140
+ "Error: #{e.message}"
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmCLI
4
+ module Commands
5
+ # Run command executes a swarm with the given configuration and prompt.
6
+ #
7
+ # Usage:
8
+ # swarm run config.yml -p "Build a REST API"
9
+ # echo "Build a REST API" | swarm run config.yml
10
+ # swarm run config.yml -p "Task" --output-format json
11
+ #
12
+ class Run
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def execute
20
+ # Validate options
21
+ options.validate!
22
+
23
+ # Load swarm configuration
24
+ swarm = load_swarm
25
+
26
+ # Create formatter based on output format
27
+ formatter = create_formatter
28
+
29
+ # Get prompt text
30
+ prompt = options.prompt_text
31
+
32
+ # Notify formatter of start
33
+ formatter.on_start(
34
+ config_path: options.config_file,
35
+ swarm_name: swarm.name,
36
+ lead_agent: swarm.lead_agent,
37
+ prompt: prompt,
38
+ )
39
+
40
+ # Execute swarm with logging
41
+ start_time = Time.now
42
+ result = swarm.execute(prompt) do |log_entry|
43
+ formatter.on_log(log_entry)
44
+ end
45
+
46
+ # Check for errors
47
+ if result.failure?
48
+ duration = Time.now - start_time
49
+ formatter.on_error(error: result.error, duration: duration)
50
+ exit(1)
51
+ end
52
+
53
+ # Notify formatter of success
54
+ formatter.on_success(result: result)
55
+
56
+ # Exit successfully
57
+ exit(0)
58
+ rescue SwarmSDK::ConfigurationError, SwarmSDK::AgentNotFoundError => e
59
+ # Configuration errors - show user-friendly message
60
+ handle_error(e, formatter: create_formatter)
61
+ exit(1)
62
+ rescue SwarmCLI::ExecutionError => e
63
+ # CLI-specific errors (e.g., missing prompt)
64
+ handle_error(e, formatter: create_formatter)
65
+ exit(1)
66
+ rescue Interrupt
67
+ # User cancelled (Ctrl+C)
68
+ $stderr.puts "\n\nExecution cancelled by user"
69
+ exit(130)
70
+ rescue StandardError => e
71
+ # Unexpected errors
72
+ handle_error(e, formatter: create_formatter)
73
+ exit(1)
74
+ end
75
+
76
+ private
77
+
78
+ def load_swarm
79
+ config_path = options.config_file
80
+
81
+ unless File.exist?(config_path)
82
+ raise SwarmCLI::ExecutionError, "Configuration file not found: #{config_path}"
83
+ end
84
+
85
+ SwarmSDK::Swarm.load(config_path)
86
+ rescue SwarmSDK::ConfigurationError => e
87
+ # Re-raise with more context
88
+ raise SwarmCLI::ExecutionError, "Configuration error: #{e.message}"
89
+ end
90
+
91
+ def create_formatter
92
+ case options.output_format
93
+ when "json"
94
+ Formatters::JsonFormatter.new
95
+ when "human"
96
+ Formatters::HumanFormatter.new(
97
+ quiet: options.quiet?,
98
+ truncate: options.truncate?,
99
+ verbose: options.verbose?,
100
+ )
101
+ else
102
+ raise SwarmCLI::ExecutionError, "Unknown output format: #{options.output_format}"
103
+ end
104
+ end
105
+
106
+ def handle_error(error, formatter: nil)
107
+ if formatter
108
+ formatter.on_error(error: error)
109
+ else
110
+ # Fallback if formatter not available
111
+ $stderr.puts "Error: #{error.message}"
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end