swarm_cli 2.1.13 → 3.0.0.alpha2

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/exe/swarm3 +11 -0
  4. data/lib/swarm_cli/v3/activity_indicator.rb +168 -0
  5. data/lib/swarm_cli/v3/ansi_colors.rb +70 -0
  6. data/lib/swarm_cli/v3/cli.rb +721 -0
  7. data/lib/swarm_cli/v3/command_completer.rb +112 -0
  8. data/lib/swarm_cli/v3/display.rb +607 -0
  9. data/lib/swarm_cli/v3/dropdown.rb +130 -0
  10. data/lib/swarm_cli/v3/event_renderer.rb +161 -0
  11. data/lib/swarm_cli/v3/file_completer.rb +143 -0
  12. data/lib/swarm_cli/v3/raw_input_reader.rb +304 -0
  13. data/lib/swarm_cli/v3/reboot_tool.rb +123 -0
  14. data/lib/swarm_cli/v3/text_input.rb +235 -0
  15. data/lib/swarm_cli/v3.rb +52 -0
  16. metadata +30 -245
  17. data/exe/swarm +0 -6
  18. data/lib/swarm_cli/cli.rb +0 -201
  19. data/lib/swarm_cli/command_registry.rb +0 -61
  20. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  21. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  22. data/lib/swarm_cli/commands/migrate.rb +0 -55
  23. data/lib/swarm_cli/commands/run.rb +0 -173
  24. data/lib/swarm_cli/config_loader.rb +0 -98
  25. data/lib/swarm_cli/formatters/human_formatter.rb +0 -811
  26. data/lib/swarm_cli/formatters/json_formatter.rb +0 -62
  27. data/lib/swarm_cli/interactive_repl.rb +0 -895
  28. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  29. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  30. data/lib/swarm_cli/migrate_options.rb +0 -54
  31. data/lib/swarm_cli/migrator.rb +0 -132
  32. data/lib/swarm_cli/options.rb +0 -151
  33. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  34. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  35. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  36. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  37. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  38. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  39. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  40. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  41. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  42. data/lib/swarm_cli/ui/icons.rb +0 -36
  43. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  44. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  45. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  46. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  47. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  48. data/lib/swarm_cli/version.rb +0 -5
  49. data/lib/swarm_cli.rb +0 -46
@@ -1,130 +0,0 @@
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.load_file(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.load_file(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
@@ -1,148 +0,0 @@
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
- # Create volatile scratchpad for MCP server
18
- # Note: Scratchpad is always volatile - data is not persisted between sessions
19
- @scratchpad = SwarmSDK::Tools::Stores::ScratchpadStorage.new
20
- end
21
-
22
- def execute
23
- # Validate options
24
- options.validate!
25
-
26
- # Determine which tools to expose
27
- tools_to_expose = determine_tools
28
-
29
- if tools_to_expose.empty?
30
- $stderr.puts "Error: No tools available to expose"
31
- exit(1)
32
- end
33
-
34
- # Start the MCP server
35
- start_mcp_server(tools_to_expose)
36
- rescue Interrupt
37
- # User cancelled (Ctrl+C) - silent exit
38
- exit(130)
39
- rescue StandardError => e
40
- # Unexpected errors - always log to stderr
41
- $stderr.puts "Fatal error: #{e.message}"
42
- $stderr.puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
43
- exit(1)
44
- end
45
-
46
- private
47
-
48
- def determine_tools
49
- if options.tool_names.any?
50
- # Use specified tools
51
- options.tool_names.map(&:to_sym)
52
- else
53
- # Default: expose all available tools
54
- SwarmSDK::Tools::Registry.available_names
55
- end
56
- end
57
-
58
- def start_mcp_server(tool_names)
59
- require "fast_mcp"
60
-
61
- # Create the server
62
- server = FastMcp::Server.new(
63
- name: "swarm-tools-server",
64
- version: SwarmCLI::VERSION,
65
- )
66
-
67
- # Register each tool
68
- tool_names.each do |tool_name|
69
- tool_class = create_mcp_tool_wrapper(tool_name)
70
- server.register_tool(tool_class)
71
- end
72
-
73
- # Start with stdio transport (default)
74
- server.start
75
- end
76
-
77
- def create_special_tool_instance(tool_name)
78
- case tool_name
79
- when :Read
80
- SwarmSDK::Tools::Read.create_for_agent(:mcp)
81
- when :Write
82
- SwarmSDK::Tools::Write.create_for_agent(:mcp)
83
- when :Edit
84
- SwarmSDK::Tools::Edit.create_for_agent(:mcp)
85
- when :MultiEdit
86
- SwarmSDK::Tools::MultiEdit.create_for_agent(:mcp)
87
- when :TodoWrite
88
- SwarmSDK::Tools::TodoWrite.create_for_agent(:mcp)
89
- when :ScratchpadWrite
90
- SwarmSDK::Tools::ScratchpadWrite.create_for_scratchpad(@scratchpad)
91
- when :ScratchpadRead
92
- SwarmSDK::Tools::ScratchpadRead.create_for_scratchpad(@scratchpad)
93
- when :ScratchpadList
94
- SwarmSDK::Tools::ScratchpadList.create_for_scratchpad(@scratchpad)
95
- else
96
- raise "Unknown special tool: #{tool_name}"
97
- end
98
- end
99
-
100
- def create_mcp_tool_wrapper(tool_name)
101
- sdk_tool_class_or_special = SwarmSDK::Tools::Registry.get(tool_name)
102
-
103
- # Get the actual tool instance for special tools
104
- sdk_tool = if sdk_tool_class_or_special == :special
105
- create_special_tool_instance(tool_name)
106
- else
107
- sdk_tool_class_or_special.new
108
- end
109
-
110
- # Get tool metadata
111
- tool_description = sdk_tool.respond_to?(:description) ? sdk_tool.description : "SwarmSDK #{tool_name} tool"
112
- tool_params = sdk_tool.class.respond_to?(:parameters) ? sdk_tool.class.parameters : {}
113
-
114
- # Create an MCP tool wrapper
115
- Class.new(FastMcp::Tool) do
116
- tool_name tool_name.to_s
117
- description tool_description
118
-
119
- # Map RubyLLM parameters to fast-mcp arguments
120
- arguments do
121
- tool_params.each do |param_name, param_obj|
122
- param_type = param_obj.type == "integer" ? :integer : :string
123
- if param_obj.required
124
- required(param_name).filled(param_type).description(param_obj.description || "")
125
- else
126
- optional(param_name).filled(param_type).description(param_obj.description || "")
127
- end
128
- end
129
- end
130
-
131
- # Capture sdk_tool in closure
132
- define_method(:call) do |**kwargs|
133
- result = sdk_tool.execute(**kwargs)
134
-
135
- # Return string output for MCP
136
- if result.is_a?(Hash)
137
- result[:output] || result[:content] || result[:files]&.join("\n") || result.to_s
138
- else
139
- result.to_s
140
- end
141
- rescue StandardError => e
142
- "Error: #{e.message}"
143
- end
144
- end
145
- end
146
- end
147
- end
148
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmCLI
4
- module Commands
5
- # Migrate command converts Claude Swarm v1 configurations to SwarmSDK v2 format.
6
- #
7
- # Usage:
8
- # swarm migrate old-config.yml
9
- # swarm migrate old-config.yml --output new-config.yml
10
- #
11
- class Migrate
12
- attr_reader :options
13
-
14
- def initialize(options)
15
- @options = options
16
- end
17
-
18
- def execute
19
- # Validate options
20
- options.validate!
21
-
22
- # Create migrator
23
- migrator = Migrator.new(options.input_file)
24
-
25
- # Perform migration
26
- migrated_yaml = migrator.migrate
27
-
28
- # Write to output file or stdout
29
- if options.output
30
- File.write(options.output, migrated_yaml)
31
- $stderr.puts "✓ Migration complete! Converted configuration saved to: #{options.output}"
32
- else
33
- puts migrated_yaml
34
- end
35
-
36
- exit(0)
37
- rescue SwarmCLI::ExecutionError => e
38
- handle_error(e)
39
- exit(1)
40
- rescue Interrupt
41
- $stderr.puts "\n\nMigration cancelled by user"
42
- exit(130)
43
- rescue StandardError => e
44
- handle_error(e)
45
- exit(1)
46
- end
47
-
48
- private
49
-
50
- def handle_error(error)
51
- $stderr.puts "Error: #{error.message}"
52
- end
53
- end
54
- end
55
- end
@@ -1,173 +0,0 @@
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
- # Supports both YAML (.yml, .yaml) and Ruby DSL (.rb) configuration files.
8
- #
9
- # Usage:
10
- # swarm run config.yml -p "Build a REST API"
11
- # swarm run config.rb -p "Build a REST API"
12
- # echo "Build a REST API" | swarm run config.yml
13
- # swarm run config.yml -p "Task" --output-format json
14
- #
15
- class Run
16
- attr_reader :options
17
-
18
- def initialize(options)
19
- @options = options
20
- end
21
-
22
- def execute
23
- # Validate options
24
- options.validate!
25
-
26
- # Load swarm configuration
27
- swarm = load_swarm
28
-
29
- # Check if interactive mode (no prompt provided and stdin is a tty)
30
- if options.interactive_mode?
31
- run_interactive_mode(swarm)
32
- else
33
- run_non_interactive_mode(swarm)
34
- end
35
- rescue SwarmSDK::ConfigurationError, SwarmSDK::AgentNotFoundError => e
36
- # Configuration errors - show user-friendly message
37
- handle_error(e, formatter: create_formatter)
38
- exit(1)
39
- rescue SwarmCLI::ExecutionError => e
40
- # CLI-specific errors (e.g., missing prompt)
41
- handle_error(e, formatter: create_formatter)
42
- exit(1)
43
- rescue Interrupt
44
- # User cancelled (Ctrl+C)
45
- $stderr.puts "\n\nExecution cancelled by user"
46
- exit(130)
47
- rescue StandardError => e
48
- # Unexpected errors
49
- handle_error(e, formatter: create_formatter)
50
- exit(1)
51
- end
52
-
53
- private
54
-
55
- def run_interactive_mode(swarm)
56
- # Launch the interactive REPL with optional initial message
57
- initial_message = options.initial_message
58
- repl = SwarmCLI::InteractiveREPL.new(
59
- swarm: swarm,
60
- options: options,
61
- initial_message: initial_message,
62
- )
63
- repl.run
64
- exit(0)
65
- end
66
-
67
- def run_non_interactive_mode(swarm)
68
- # Create formatter based on output format
69
- formatter = create_formatter
70
-
71
- # Get prompt text
72
- prompt = options.prompt_text
73
-
74
- # Notify formatter of start
75
- formatter.on_start(
76
- config_path: options.config_file,
77
- swarm_name: swarm.name,
78
- lead_agent: swarm.lead_agent,
79
- prompt: prompt,
80
- )
81
-
82
- # Emit validation warnings before execution
83
- emit_validation_warnings(swarm, formatter)
84
-
85
- # Execute swarm with logging
86
- start_time = Time.now
87
- result = swarm.execute(prompt) do |log_entry|
88
- # Skip model warnings - already emitted above
89
- next if log_entry[:type] == "model_lookup_warning"
90
-
91
- formatter.on_log(log_entry)
92
- end
93
-
94
- # Check for errors
95
- if result.failure?
96
- duration = Time.now - start_time
97
- formatter.on_error(error: result.error, duration: duration)
98
- exit(1)
99
- end
100
-
101
- # Notify formatter of success
102
- formatter.on_success(result: result)
103
-
104
- # Exit successfully
105
- exit(0)
106
- end
107
-
108
- def load_swarm
109
- config_path = options.config_file
110
-
111
- ConfigLoader.load(config_path)
112
- rescue SwarmCLI::ConfigurationError => e
113
- # ConfigLoader already provides good context
114
- raise SwarmCLI::ExecutionError, e.message
115
- rescue SwarmSDK::ConfigurationError => e
116
- # SDK errors - add context
117
- raise SwarmCLI::ExecutionError, "Configuration error: #{e.message}"
118
- end
119
-
120
- def create_formatter
121
- case options.output_format
122
- when "json"
123
- Formatters::JsonFormatter.new
124
- when "human"
125
- Formatters::HumanFormatter.new(
126
- quiet: options.quiet?,
127
- truncate: options.truncate?,
128
- verbose: options.verbose?,
129
- )
130
- else
131
- raise SwarmCLI::ExecutionError, "Unknown output format: #{options.output_format}"
132
- end
133
- end
134
-
135
- def emit_validation_warnings(swarm, formatter)
136
- # Setup temporary logging to capture and emit warnings
137
- SwarmSDK::LogCollector.subscribe(filter: { type: "model_lookup_warning" }) do |log_entry|
138
- formatter.on_log(log_entry)
139
- end
140
-
141
- SwarmSDK::LogStream.emitter = SwarmSDK::LogCollector
142
-
143
- # Emit validation warnings as log events
144
- swarm.emit_validation_warnings
145
-
146
- # Clean up
147
- SwarmSDK::LogCollector.reset!
148
- SwarmSDK::LogStream.reset!
149
- rescue StandardError
150
- # Ignore errors during validation emission
151
- begin
152
- SwarmSDK::LogCollector.reset!
153
- rescue
154
- nil
155
- end
156
- begin
157
- SwarmSDK::LogStream.reset!
158
- rescue
159
- nil
160
- end
161
- end
162
-
163
- def handle_error(error, formatter: nil)
164
- if formatter
165
- formatter.on_error(error: error)
166
- else
167
- # Fallback if formatter not available
168
- $stderr.puts "Error: #{error.message}"
169
- end
170
- end
171
- end
172
- end
173
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmCLI
4
- # ConfigLoader handles loading swarm configurations from both YAML and Ruby DSL files.
5
- #
6
- # Supports:
7
- # - YAML files (.yml, .yaml) - loaded via SwarmSDK.load_file
8
- # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::Workflow instance
9
- #
10
- # @example Load YAML config
11
- # swarm = ConfigLoader.load("config.yml")
12
- #
13
- # @example Load Ruby DSL config
14
- # swarm = ConfigLoader.load("config.rb")
15
- #
16
- class ConfigLoader
17
- class << self
18
- # Load a swarm configuration from file (YAML or Ruby DSL)
19
- #
20
- # Detects file type by extension:
21
- # - .yml, .yaml -> Load as YAML using SwarmSDK.load_file
22
- # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::Workflow instance
23
- #
24
- # @param path [String, Pathname] Path to configuration file
25
- # @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
26
- # @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
27
- def load(path)
28
- path = Pathname.new(path).expand_path
29
-
30
- unless path.exist?
31
- raise ConfigurationError, "Configuration file not found: #{path}"
32
- end
33
-
34
- case path.extname.downcase
35
- when ".yml", ".yaml"
36
- load_yaml(path)
37
- when ".rb"
38
- load_ruby_dsl(path)
39
- else
40
- raise ConfigurationError,
41
- "Unsupported configuration file format: #{path.extname}. " \
42
- "Supported formats: .yml, .yaml (YAML), .rb (Ruby DSL)"
43
- end
44
- end
45
-
46
- private
47
-
48
- # Load YAML configuration file
49
- #
50
- # @param path [Pathname] Path to YAML file
51
- # @return [SwarmSDK::Swarm] Configured swarm instance
52
- def load_yaml(path)
53
- SwarmSDK.load_file(path.to_s)
54
- rescue SwarmSDK::ConfigurationError => e
55
- # Re-raise with CLI context
56
- raise ConfigurationError, "Configuration error in #{path}: #{e.message}"
57
- end
58
-
59
- # Load Ruby DSL configuration file
60
- #
61
- # Executes the Ruby file in a clean binding and expects it to return
62
- # a SwarmSDK::Swarm or SwarmSDK::Workflow instance. The file should
63
- # use SwarmSDK.build or SwarmSDK.workflow or create a Swarm/Workflow instance directly.
64
- #
65
- # @param path [Pathname] Path to Ruby DSL file
66
- # @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
67
- # @raise [ConfigurationError] If file doesn't return a valid instance
68
- def load_ruby_dsl(path)
69
- # Read the file content
70
- content = path.read
71
-
72
- # Execute in a clean binding with SwarmSDK available
73
- # This allows the DSL file to use SwarmSDK.build or SwarmSDK.workflow directly
74
- result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
75
-
76
- # Validate result is a Swarm or Workflow instance
77
- # Both have the same execute(prompt) interface
78
- unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::Workflow)
79
- raise ConfigurationError,
80
- "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::Workflow instance. " \
81
- "Got: #{result.class}. " \
82
- "Use: SwarmSDK.build { ... } or SwarmSDK.workflow { ... }"
83
- end
84
-
85
- result
86
- rescue SwarmSDK::ConfigurationError => e
87
- # Re-raise SDK configuration errors with CLI context
88
- raise ConfigurationError, "Configuration error in #{path}: #{e.message}"
89
- rescue SyntaxError => e
90
- # Ruby syntax errors
91
- raise ConfigurationError, "Syntax error in #{path}: #{e.message}"
92
- rescue StandardError => e
93
- # Other Ruby errors during execution
94
- raise ConfigurationError, "Error loading #{path}: #{e.message}"
95
- end
96
- end
97
- end
98
- end