claude_swarm 0.1.0

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.
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module ClaudeSwarm
7
+ class ClaudeCodeExecutor
8
+ attr_reader :session_id, :last_response, :working_directory
9
+
10
+ def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false)
11
+ @working_directory = working_directory
12
+ @model = model
13
+ @mcp_config = mcp_config
14
+ @vibe = vibe
15
+ @session_id = nil
16
+ @last_response = nil
17
+ end
18
+
19
+ def execute(prompt, options = {})
20
+ cmd_array = build_command_array(prompt, options)
21
+
22
+ stdout, stderr, status = Open3.capture3(*cmd_array, chdir: @working_directory)
23
+
24
+ raise ExecutionError, "Claude Code execution failed: #{stderr}" unless status.success?
25
+
26
+ begin
27
+ response = JSON.parse(stdout)
28
+ @last_response = response
29
+
30
+ # Extract and store session ID from the response
31
+ @session_id = response["session_id"]
32
+
33
+ response
34
+ rescue JSON::ParserError => e
35
+ raise ParseError, "Failed to parse JSON response: #{e.message}\nOutput: #{stdout}"
36
+ end
37
+ end
38
+
39
+ def execute_text(prompt, options = {})
40
+ response = execute(prompt, options)
41
+ response["result"] || ""
42
+ end
43
+
44
+ def reset_session
45
+ @session_id = nil
46
+ @last_response = nil
47
+ end
48
+
49
+ def has_session?
50
+ !@session_id.nil?
51
+ end
52
+
53
+ private
54
+
55
+ def build_command_array(prompt, options)
56
+ cmd_array = ["claude"]
57
+
58
+ # Add model if specified
59
+ cmd_array += ["--model", @model]
60
+
61
+ # Add MCP config if specified
62
+ cmd_array += ["--mcp-config", @mcp_config] if @mcp_config
63
+
64
+ # Resume session if we have a session ID
65
+ cmd_array += ["--resume", @session_id] if @session_id && !options[:new_session]
66
+
67
+ # Always use JSON output format for structured responses
68
+ cmd_array += ["--output-format", "json"]
69
+
70
+ # Add non-interactive mode
71
+ cmd_array << "--print"
72
+
73
+ # Add any custom system prompt
74
+ cmd_array += ["--system-prompt", options[:system_prompt]] if options[:system_prompt]
75
+
76
+ # Add any allowed tools or vibe flag
77
+ if @vibe
78
+ cmd_array << "--dangerously-skip-permissions"
79
+ elsif options[:allowed_tools]
80
+ tools = Array(options[:allowed_tools]).join(",")
81
+ cmd_array += ["--allowedTools", tools]
82
+ end
83
+
84
+ # Add the prompt as the last argument (no escaping needed with array syntax)
85
+ cmd_array << prompt
86
+
87
+ cmd_array
88
+ end
89
+
90
+ class ExecutionError < StandardError; end
91
+ class ParseError < StandardError; end
92
+ end
93
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast_mcp"
4
+ require "json"
5
+ require "fileutils"
6
+ require "logger"
7
+ require_relative "claude_code_executor"
8
+
9
+ module ClaudeSwarm
10
+ class ClaudeMcpServer
11
+ SWARM_DIR = ".claude-swarm"
12
+ LOGS_DIR = "logs"
13
+
14
+ # Class variables to share state with tool classes
15
+ class << self
16
+ attr_accessor :executor, :instance_config, :logger, :session_timestamp
17
+ end
18
+
19
+ def initialize(instance_config)
20
+ @instance_config = instance_config
21
+ @executor = ClaudeCodeExecutor.new(
22
+ working_directory: instance_config[:directory],
23
+ model: instance_config[:model],
24
+ mcp_config: instance_config[:mcp_config_path],
25
+ vibe: instance_config[:vibe]
26
+ )
27
+
28
+ # Setup logging
29
+ setup_logging
30
+
31
+ # Set class variables so tools can access them
32
+ self.class.executor = @executor
33
+ self.class.instance_config = @instance_config
34
+ self.class.logger = @logger
35
+ end
36
+
37
+ private
38
+
39
+ def setup_logging
40
+ # Use environment variable for session timestamp if available (set by orchestrator)
41
+ # Otherwise create a new timestamp
42
+ self.class.session_timestamp ||= ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
43
+
44
+ # Ensure the logs directory exists
45
+ logs_dir = File.join(Dir.pwd, SWARM_DIR, LOGS_DIR)
46
+ FileUtils.mkdir_p(logs_dir)
47
+
48
+ # Create logger with timestamped filename
49
+ log_filename = "session_#{self.class.session_timestamp}.log"
50
+ log_path = File.join(logs_dir, log_filename)
51
+ @logger = Logger.new(log_path)
52
+ @logger.level = Logger::INFO
53
+
54
+ # Custom formatter for better readability
55
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
56
+ "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
57
+ end
58
+
59
+ @logger.info("Started MCP server for instance: #{@instance_config[:name]}")
60
+ end
61
+
62
+ public
63
+
64
+ def start
65
+ server = FastMcp::Server.new(
66
+ name: @instance_config[:name],
67
+ version: "1.0.0"
68
+ )
69
+
70
+ # Register tool classes (not instances)
71
+ server.register_tool(TaskTool)
72
+ server.register_tool(SessionInfoTool)
73
+ server.register_tool(ResetSessionTool)
74
+
75
+ # Start the stdio server
76
+ server.start
77
+ end
78
+
79
+ class TaskTool < FastMcp::Tool
80
+ tool_name "task"
81
+ description "Execute a task using Claude Code"
82
+
83
+ arguments do
84
+ required(:prompt).filled(:string).description("The task or question for Claude")
85
+ optional(:new_session).filled(:bool).description("Start a new session (default: false)")
86
+ optional(:system_prompt).filled(:string).description("Override the system prompt for this request")
87
+ end
88
+
89
+ def call(prompt:, new_session: false, system_prompt: nil)
90
+ executor = ClaudeMcpServer.executor
91
+ instance_config = ClaudeMcpServer.instance_config
92
+ logger = ClaudeMcpServer.logger
93
+
94
+ options = {
95
+ new_session: new_session,
96
+ system_prompt: system_prompt || instance_config[:prompt]
97
+ }
98
+
99
+ # Add allowed tools from instance config
100
+ options[:allowed_tools] = instance_config[:tools] if instance_config[:tools]&.any?
101
+
102
+ begin
103
+ # Log the request
104
+ log_entry = {
105
+ timestamp: Time.now.utc.iso8601,
106
+ instance_name: instance_config[:name],
107
+ model: instance_config[:model],
108
+ working_directory: instance_config[:directory],
109
+ session_id: executor.session_id,
110
+ request: {
111
+ prompt: prompt,
112
+ new_session: new_session,
113
+ system_prompt: options[:system_prompt],
114
+ allowed_tools: options[:allowed_tools]
115
+ }
116
+ }
117
+
118
+ logger.info("REQUEST: #{JSON.pretty_generate(log_entry)}")
119
+
120
+ response = executor.execute(prompt, options)
121
+
122
+ # Log the response
123
+ response_entry = log_entry.merge(
124
+ session_id: executor.session_id, # Update with new session ID if changed
125
+ response: {
126
+ result: response["result"],
127
+ cost_usd: response["cost_usd"],
128
+ duration_ms: response["duration_ms"],
129
+ is_error: response["is_error"],
130
+ total_cost: response["total_cost"]
131
+ }
132
+ )
133
+
134
+ logger.info("RESPONSE: #{JSON.pretty_generate(response_entry)}")
135
+
136
+ # Return just the result text as expected by MCP
137
+ response["result"]
138
+ rescue ClaudeCodeExecutor::ExecutionError => e
139
+ logger.error("Execution error for #{instance_config[:name]}: #{e.message}")
140
+ raise StandardError, "Execution failed: #{e.message}"
141
+ rescue ClaudeCodeExecutor::ParseError => e
142
+ logger.error("Parse error for #{instance_config[:name]}: #{e.message}")
143
+ raise StandardError, "Parse error: #{e.message}"
144
+ rescue StandardError => e
145
+ logger.error("Unexpected error for #{instance_config[:name]}: #{e.class} - #{e.message}")
146
+ logger.error("Backtrace: #{e.backtrace.join("\n")}")
147
+ raise StandardError, "Unexpected error: #{e.message}"
148
+ end
149
+ end
150
+ end
151
+
152
+ class SessionInfoTool < FastMcp::Tool
153
+ tool_name "session_info"
154
+ description "Get information about the current Claude session"
155
+
156
+ arguments do
157
+ # No arguments needed
158
+ end
159
+
160
+ def call
161
+ executor = ClaudeMcpServer.executor
162
+
163
+ {
164
+ has_session: executor.has_session?,
165
+ session_id: executor.session_id,
166
+ working_directory: executor.working_directory
167
+ }
168
+ end
169
+ end
170
+
171
+ class ResetSessionTool < FastMcp::Tool
172
+ tool_name "reset_session"
173
+ description "Reset the Claude session, starting fresh on the next task"
174
+
175
+ arguments do
176
+ # No arguments needed
177
+ end
178
+
179
+ def call
180
+ executor = ClaudeMcpServer.executor
181
+ executor.reset_session
182
+
183
+ {
184
+ success: true,
185
+ message: "Session has been reset"
186
+ }
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "configuration"
5
+ require_relative "mcp_generator"
6
+ require_relative "orchestrator"
7
+ require_relative "claude_mcp_server"
8
+
9
+ module ClaudeSwarm
10
+ class CLI < Thor
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ desc "start [CONFIG_FILE]", "Start a Claude Swarm from configuration file"
16
+ method_option :config, aliases: "-c", type: :string, default: "claude-swarm.yml",
17
+ desc: "Path to configuration file"
18
+ method_option :vibe, type: :boolean, default: false,
19
+ desc: "Run with --dangerously-skip-permissions for all instances"
20
+ def start(config_file = nil)
21
+ config_path = config_file || options[:config]
22
+ unless File.exist?(config_path)
23
+ error "Configuration file not found: #{config_path}"
24
+ exit 1
25
+ end
26
+
27
+ say "Starting Claude Swarm from #{config_path}..."
28
+ begin
29
+ config = Configuration.new(config_path)
30
+ generator = McpGenerator.new(config, vibe: options[:vibe])
31
+ orchestrator = Orchestrator.new(config, generator, vibe: options[:vibe])
32
+ orchestrator.start
33
+ rescue Error => e
34
+ error e.message
35
+ exit 1
36
+ rescue StandardError => e
37
+ error "Unexpected error: #{e.message}"
38
+ error e.backtrace.join("\n") if options[:verbose]
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ desc "mcp-serve", "Start an MCP server for a Claude instance"
44
+ method_option :name, aliases: "-n", type: :string, required: true,
45
+ desc: "Instance name"
46
+ method_option :directory, aliases: "-d", type: :string, required: true,
47
+ desc: "Working directory for the instance"
48
+ method_option :model, aliases: "-m", type: :string, required: true,
49
+ desc: "Claude model to use (e.g., opus, sonnet)"
50
+ method_option :prompt, aliases: "-p", type: :string,
51
+ desc: "System prompt for the instance"
52
+ method_option :tools, aliases: "-t", type: :array,
53
+ desc: "Allowed tools for the instance"
54
+ method_option :mcp_config_path, type: :string,
55
+ desc: "Path to MCP configuration file"
56
+ method_option :debug, type: :boolean, default: false,
57
+ desc: "Enable debug output"
58
+ method_option :vibe, type: :boolean, default: false,
59
+ desc: "Run with --dangerously-skip-permissions"
60
+ def mcp_serve
61
+ instance_config = {
62
+ name: options[:name],
63
+ directory: options[:directory],
64
+ model: options[:model],
65
+ prompt: options[:prompt],
66
+ tools: options[:tools] || [],
67
+ mcp_config_path: options[:mcp_config_path],
68
+ vibe: options[:vibe]
69
+ }
70
+
71
+ begin
72
+ server = ClaudeMcpServer.new(instance_config)
73
+ server.start
74
+ rescue StandardError => e
75
+ error "Error starting MCP server: #{e.message}"
76
+ error e.backtrace.join("\n") if options[:debug]
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ desc "version", "Show Claude Swarm version"
82
+ def version
83
+ say "Claude Swarm #{VERSION}"
84
+ end
85
+
86
+ default_task :start
87
+
88
+ private
89
+
90
+ def error(message)
91
+ say message, :red
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ module ClaudeSwarm
7
+ class Configuration
8
+ attr_reader :config, :swarm_name, :main_instance, :instances
9
+
10
+ def initialize(config_path)
11
+ @config_path = Pathname.new(config_path).expand_path
12
+ @config_dir = @config_path.dirname
13
+ load_and_validate
14
+ end
15
+
16
+ def main_instance_config
17
+ instances[main_instance]
18
+ end
19
+
20
+ def instance_names
21
+ instances.keys
22
+ end
23
+
24
+ def connections_for(instance_name)
25
+ instances[instance_name][:connections] || []
26
+ end
27
+
28
+ private
29
+
30
+ def load_and_validate
31
+ @config = YAML.load_file(@config_path)
32
+ validate_version
33
+ validate_swarm
34
+ parse_swarm
35
+ validate_directories
36
+ rescue Errno::ENOENT
37
+ raise Error, "Configuration file not found: #{@config_path}"
38
+ rescue Psych::SyntaxError => e
39
+ raise Error, "Invalid YAML syntax: #{e.message}"
40
+ end
41
+
42
+ def validate_version
43
+ version = @config["version"]
44
+ raise Error, "Missing 'version' field in configuration" unless version
45
+ raise Error, "Unsupported version: #{version}. Only version 1 is supported" unless version == 1
46
+ end
47
+
48
+ def validate_swarm
49
+ raise Error, "Missing 'swarm' field in configuration" unless @config["swarm"]
50
+
51
+ swarm = @config["swarm"]
52
+ raise Error, "Missing 'name' field in swarm configuration" unless swarm["name"]
53
+ raise Error, "Missing 'instances' field in swarm configuration" unless swarm["instances"]
54
+ raise Error, "Missing 'main' field in swarm configuration" unless swarm["main"]
55
+
56
+ raise Error, "No instances defined" if swarm["instances"].empty?
57
+
58
+ main = swarm["main"]
59
+ raise Error, "Main instance '#{main}' not found in instances" unless swarm["instances"].key?(main)
60
+ end
61
+
62
+ def parse_swarm
63
+ swarm = @config["swarm"]
64
+ @swarm_name = swarm["name"]
65
+ @main_instance = swarm["main"]
66
+ @instances = {}
67
+ swarm["instances"].each do |name, config|
68
+ @instances[name] = parse_instance(name, config)
69
+ end
70
+ validate_connections
71
+ end
72
+
73
+ def parse_instance(name, config)
74
+ config ||= {}
75
+ {
76
+ name: name,
77
+ role: config["role"] || name.to_s.tr("_", " ").capitalize,
78
+ directory: expand_path(config["directory"] || "."),
79
+ model: config["model"] || "sonnet",
80
+ connections: Array(config["connections"]),
81
+ tools: Array(config["tools"]),
82
+ mcps: parse_mcps(config["mcps"] || []),
83
+ prompt: config["prompt"]
84
+ }
85
+ end
86
+
87
+ def parse_mcps(mcps)
88
+ mcps.map do |mcp|
89
+ validate_mcp(mcp)
90
+ mcp
91
+ end
92
+ end
93
+
94
+ def validate_mcp(mcp)
95
+ raise Error, "MCP configuration missing 'name'" unless mcp["name"]
96
+
97
+ case mcp["type"]
98
+ when "stdio"
99
+ raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
100
+ when "sse"
101
+ raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
102
+ else
103
+ raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
104
+ end
105
+ end
106
+
107
+ def validate_connections
108
+ @instances.each do |name, instance|
109
+ instance[:connections].each do |connection|
110
+ raise Error, "Instance '#{name}' has connection to unknown instance '#{connection}'" unless @instances.key?(connection)
111
+ end
112
+ end
113
+ end
114
+
115
+ def validate_directories
116
+ @instances.each do |name, instance|
117
+ directory = instance[:directory]
118
+ raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
119
+ end
120
+ end
121
+
122
+ def expand_path(path)
123
+ Pathname.new(path).expand_path(@config_dir).to_s
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "shellwords"
6
+
7
+ module ClaudeSwarm
8
+ class McpGenerator
9
+ SWARM_DIR = ".claude-swarm"
10
+
11
+ def initialize(configuration, vibe: false)
12
+ @config = configuration
13
+ @vibe = vibe
14
+ end
15
+
16
+ def generate_all
17
+ ensure_swarm_directory
18
+
19
+ @config.instances.each do |name, instance|
20
+ generate_mcp_config(name, instance)
21
+ end
22
+ end
23
+
24
+ def mcp_config_path(instance_name)
25
+ File.join(Dir.pwd, SWARM_DIR, "#{instance_name}.mcp.json")
26
+ end
27
+
28
+ private
29
+
30
+ def swarm_dir
31
+ File.join(Dir.pwd, SWARM_DIR)
32
+ end
33
+
34
+ def ensure_swarm_directory
35
+ FileUtils.mkdir_p(swarm_dir)
36
+
37
+ # Create logs directory as well
38
+ logs_dir = File.join(swarm_dir, "logs")
39
+ FileUtils.mkdir_p(logs_dir)
40
+
41
+ gitignore_path = File.join(swarm_dir, ".gitignore")
42
+ File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
43
+ end
44
+
45
+ def generate_mcp_config(name, instance)
46
+ mcp_servers = {}
47
+
48
+ # Add configured MCP servers
49
+ instance[:mcps].each do |mcp|
50
+ mcp_servers[mcp["name"]] = build_mcp_server_config(mcp)
51
+ end
52
+
53
+ # Add connection MCPs for other instances
54
+ instance[:connections].each do |connection_name|
55
+ connected_instance = @config.instances[connection_name]
56
+ mcp_servers[connection_name] = build_instance_mcp_config(connection_name, connected_instance)
57
+ end
58
+
59
+ config = {
60
+ "mcpServers" => mcp_servers
61
+ }
62
+
63
+ File.write(mcp_config_path(name), JSON.pretty_generate(config))
64
+ end
65
+
66
+ def build_mcp_server_config(mcp)
67
+ case mcp["type"]
68
+ when "stdio"
69
+ {
70
+ "type" => "stdio",
71
+ "command" => mcp["command"],
72
+ "args" => mcp["args"] || []
73
+ }.tap do |config|
74
+ config["env"] = mcp["env"] if mcp["env"]
75
+ end
76
+ when "sse"
77
+ {
78
+ "type" => "sse",
79
+ "url" => mcp["url"]
80
+ }
81
+ end
82
+ end
83
+
84
+ def build_instance_mcp_config(name, instance)
85
+ # Get the path to the claude-swarm executable
86
+ exe_path = "claude-swarm"
87
+
88
+ # Build command-line arguments for Thor
89
+ args = [
90
+ "mcp-serve",
91
+ "--name", name,
92
+ "--directory", instance[:directory],
93
+ "--model", instance[:model]
94
+ ]
95
+
96
+ # Add optional arguments
97
+ args.push("--prompt", instance[:prompt]) if instance[:prompt]
98
+
99
+ args.push("--tools", instance[:tools].join(",")) if instance[:tools] && !instance[:tools].empty?
100
+
101
+ args.push("--mcp-config-path", mcp_config_path(name))
102
+
103
+ args.push("--vibe") if @vibe
104
+
105
+ {
106
+ "type" => "stdio",
107
+ "command" => exe_path,
108
+ "args" => args
109
+ }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module ClaudeSwarm
6
+ class Orchestrator
7
+ def initialize(configuration, mcp_generator, vibe: false)
8
+ @config = configuration
9
+ @generator = mcp_generator
10
+ @vibe = vibe
11
+ end
12
+
13
+ def start
14
+ puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
15
+ puts "😎 Vibe mode ON" if @vibe
16
+ puts
17
+
18
+ # Set session timestamp for all instances to share the same log file
19
+ session_timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
20
+ ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] = session_timestamp
21
+ puts "📝 Session logs will be saved to: .claude-swarm/logs/session_#{session_timestamp}.log"
22
+ puts
23
+
24
+ # Generate all MCP configuration files
25
+ @generator.generate_all
26
+ puts "✓ Generated MCP configurations in .claude-swarm/"
27
+ puts
28
+
29
+ # Launch the main instance
30
+ main_instance = @config.main_instance_config
31
+ puts "🚀 Launching main instance: #{@config.main_instance} (#{main_instance[:role]})"
32
+ puts " Model: #{main_instance[:model]}"
33
+ puts " Directory: #{main_instance[:directory]}"
34
+ puts " Tools: #{main_instance[:tools].join(", ")}" if main_instance[:tools].any?
35
+ puts " Connections: #{main_instance[:connections].join(", ")}" if main_instance[:connections].any?
36
+ puts
37
+
38
+ command = build_main_command(main_instance)
39
+ if ENV["DEBUG"]
40
+ puts "Running: #{command}"
41
+ puts
42
+ end
43
+
44
+ # Execute the main instance - this will cascade to other instances via MCP
45
+ exec(command)
46
+ end
47
+
48
+ private
49
+
50
+ def build_main_command(instance)
51
+ parts = []
52
+ parts << "cd #{Shellwords.escape(instance[:directory])} &&"
53
+ parts << "claude"
54
+ parts << "--model #{instance[:model]}"
55
+
56
+ if @vibe
57
+ parts << "--dangerously-skip-permissions"
58
+ elsif instance[:tools].any?
59
+ tools_str = instance[:tools].join(",")
60
+ parts << "--allowedTools '#{tools_str}'"
61
+ end
62
+
63
+ parts << "--append-system-prompt #{Shellwords.escape(instance[:prompt])}" if instance[:prompt]
64
+
65
+ mcp_config_path = @generator.mcp_config_path(@config.main_instance)
66
+ parts << "--mcp-config #{mcp_config_path}"
67
+
68
+ parts.join(" ")
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "claude_swarm/version"
4
+ require_relative "claude_swarm/cli"
5
+ require_relative "claude_swarm/claude_code_executor"
6
+ require_relative "claude_swarm/claude_mcp_server"
7
+
8
+ module ClaudeSwarm
9
+ class Error < StandardError; end
10
+ end