claude_swarm 0.3.2 → 1.0.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.
@@ -4,26 +4,118 @@ module ClaudeSwarm
4
4
  module SessionCostCalculator
5
5
  extend self
6
6
 
7
+ # Model pricing in dollars per million tokens
8
+ MODEL_PRICING = {
9
+ opus: {
10
+ input: 15.0,
11
+ output: 75.0,
12
+ cache_write: 18.75,
13
+ cache_read: 1.50,
14
+ },
15
+ sonnet: {
16
+ input: 3.0,
17
+ output: 15.0,
18
+ cache_write: 3.75,
19
+ cache_read: 0.30,
20
+ },
21
+ haiku: {
22
+ input: 0.80,
23
+ output: 4.0,
24
+ cache_write: 1.0,
25
+ cache_read: 0.08,
26
+ },
27
+ }.freeze
28
+
29
+ # Determine model type from model name
30
+ def model_type_from_name(model_name)
31
+ return unless model_name
32
+
33
+ model_name_lower = model_name.downcase
34
+ if model_name_lower.include?("opus")
35
+ :opus
36
+ elsif model_name_lower.include?("sonnet")
37
+ :sonnet
38
+ elsif model_name_lower.include?("haiku")
39
+ :haiku
40
+ end
41
+ end
42
+
43
+ # Calculate cost from token usage
44
+ def calculate_token_cost(usage, model_name)
45
+ model_type = model_type_from_name(model_name)
46
+ return 0.0 unless model_type && usage
47
+
48
+ pricing = MODEL_PRICING[model_type]
49
+ return 0.0 unless pricing
50
+
51
+ cost = 0.0
52
+
53
+ # Regular input tokens
54
+ if usage["input_tokens"]
55
+ cost += (usage["input_tokens"] / 1_000_000.0) * pricing[:input]
56
+ end
57
+
58
+ # Output tokens
59
+ if usage["output_tokens"]
60
+ cost += (usage["output_tokens"] / 1_000_000.0) * pricing[:output]
61
+ end
62
+
63
+ # Cache creation tokens (write)
64
+ if usage["cache_creation_input_tokens"]
65
+ cost += (usage["cache_creation_input_tokens"] / 1_000_000.0) * pricing[:cache_write]
66
+ end
67
+
68
+ # Cache read tokens
69
+ if usage["cache_read_input_tokens"]
70
+ cost += (usage["cache_read_input_tokens"] / 1_000_000.0) * pricing[:cache_read]
71
+ end
72
+
73
+ cost
74
+ end
75
+
7
76
  # Calculate total cost from session log file
8
77
  # Returns a hash with:
9
- # - total_cost: Total cost in USD
78
+ # - total_cost: Total cost in USD (sum of cost_usd for instances, token costs for main)
10
79
  # - instances_with_cost: Set of instance names that have cost data
11
80
  def calculate_total_cost(session_log_path)
12
81
  return { total_cost: 0.0, instances_with_cost: Set.new } unless File.exist?(session_log_path)
13
82
 
14
- total_cost = 0.0
83
+ # Track costs per instance - simple sum of cost_usd
84
+ instance_costs = {}
15
85
  instances_with_cost = Set.new
86
+ main_instance_cost = 0.0
16
87
 
17
88
  File.foreach(session_log_path) do |line|
18
- data = JSON.parse(line)
19
- if data.dig("event", "type") == "result" && (cost = data.dig("event", "total_cost_usd"))
20
- total_cost += cost
21
- instances_with_cost << data["instance"]
89
+ data = JsonHandler.parse(line)
90
+ next if data == line # Skip unparseable lines
91
+
92
+ instance_name = data["instance"]
93
+ instance_id = data["instance_id"]
94
+
95
+ # Handle main instance token-based costs
96
+ if instance_id == "main" && data.dig("event", "type") == "assistant"
97
+ usage = data.dig("event", "message", "usage")
98
+ model = data.dig("event", "message", "model")
99
+ if usage && model
100
+ token_cost = calculate_token_cost(usage, model)
101
+ main_instance_cost += token_cost
102
+ instances_with_cost << instance_name if token_cost > 0
103
+ end
104
+ # Handle other instances with cost_usd (non-cumulative)
105
+ elsif instance_id != "main" && data.dig("event", "type") == "result"
106
+ # Use cost_usd (non-cumulative) instead of total_cost_usd (cumulative)
107
+ if (cost = data.dig("event", "cost_usd"))
108
+ instances_with_cost << instance_name
109
+ instance_costs[instance_name] ||= 0.0
110
+ instance_costs[instance_name] += cost
111
+ end
22
112
  end
23
- rescue JSON::ParserError
24
- next
25
113
  end
26
114
 
115
+ # Calculate total: sum of all instance costs + main instance token costs
116
+ other_instances_cost = instance_costs.values.sum
117
+ total_cost = other_instances_cost + main_instance_cost
118
+
27
119
  {
28
120
  total_cost: total_cost,
29
121
  instances_with_cost: instances_with_cost,
@@ -39,11 +131,15 @@ module ClaudeSwarm
39
131
  # Returns a hash of instances with their cost data and relationships
40
132
  def parse_instance_hierarchy(session_log_path)
41
133
  instances = {}
134
+ # Track main instance token costs
135
+ main_instance_costs = {}
42
136
 
43
137
  return instances unless File.exist?(session_log_path)
44
138
 
45
139
  File.foreach(session_log_path) do |line|
46
- data = JSON.parse(line)
140
+ data = JsonHandler.parse(line)
141
+ next if data == line # Skip unparseable lines
142
+
47
143
  instance_name = data["instance"]
48
144
  instance_id = data["instance_id"]
49
145
  calling_instance = data["calling_instance"]
@@ -75,16 +171,36 @@ module ClaudeSwarm
75
171
  instances[calling_instance][:calls_to] << instance_name
76
172
  end
77
173
 
78
- # Track costs and calls
79
- if data.dig("event", "type") == "result"
174
+ # Handle main instance token-based costs
175
+ if instance_id == "main" && data.dig("event", "type") == "assistant"
176
+ usage = data.dig("event", "message", "usage")
177
+ model = data.dig("event", "message", "model")
178
+ if usage && model
179
+ token_cost = calculate_token_cost(usage, model)
180
+ if token_cost > 0
181
+ main_instance_costs[instance_name] ||= 0.0
182
+ main_instance_costs[instance_name] += token_cost
183
+ instances[instance_name][:has_cost_data] = true
184
+ instances[instance_name][:calls] += 1
185
+ end
186
+ end
187
+ # Track costs and calls for non-main instances using cost_usd
188
+ elsif data.dig("event", "type") == "result" && instance_id != "main"
80
189
  instances[instance_name][:calls] += 1
81
- if (cost = data.dig("event", "total_cost_usd"))
190
+ # Use cost_usd (non-cumulative) instead of total_cost_usd
191
+ if (cost = data.dig("event", "cost_usd"))
82
192
  instances[instance_name][:cost] += cost
83
193
  instances[instance_name][:has_cost_data] = true
84
194
  end
85
195
  end
86
- rescue JSON::ParserError
87
- next
196
+ end
197
+
198
+ # Set main instance costs (replace, don't add)
199
+ main_instance_costs.each do |name, cost|
200
+ if instances[name]
201
+ # For main instances, use ONLY token costs, not cumulative costs
202
+ instances[name][:cost] = cost
203
+ end
88
204
  end
89
205
 
90
206
  instances
@@ -2,13 +2,7 @@
2
2
 
3
3
  module ClaudeSwarm
4
4
  module SessionPath
5
- SESSIONS_DIR = "sessions"
6
-
7
5
  class << self
8
- def swarm_home
9
- ENV["CLAUDE_SWARM_HOME"] || File.expand_path("~/.claude-swarm")
10
- end
11
-
12
6
  # Convert a directory path to a safe folder name using + as separator
13
7
  def project_folder_name(working_dir = Dir.pwd)
14
8
  # Don't expand path if it's already expanded (avoids double expansion on Windows)
@@ -27,7 +21,7 @@ module ClaudeSwarm
27
21
  # Generate a full session path for a given directory and session ID
28
22
  def generate(working_dir: Dir.pwd, session_id: SecureRandom.uuid)
29
23
  project_name = project_folder_name(working_dir)
30
- File.join(swarm_home, SESSIONS_DIR, project_name, session_id)
24
+ ClaudeSwarm.joined_sessions_dir(project_name, session_id)
31
25
  end
32
26
 
33
27
  # Ensure the session directory exists
@@ -35,7 +29,7 @@ module ClaudeSwarm
35
29
  FileUtils.mkdir_p(session_path)
36
30
 
37
31
  # Add .gitignore to swarm home if it doesn't exist
38
- gitignore_path = File.join(swarm_home, ".gitignore")
32
+ gitignore_path = ClaudeSwarm.joined_home_dir(".gitignore")
39
33
  File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
40
34
  end
41
35
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class SettingsGenerator
5
+ def initialize(configuration)
6
+ @config = configuration
7
+ end
8
+
9
+ def generate_all
10
+ ensure_session_directory
11
+
12
+ @config.instances.each do |name, instance|
13
+ generate_settings(name, instance)
14
+ end
15
+ end
16
+
17
+ def settings_path(instance_name)
18
+ File.join(session_path, "#{instance_name}_settings.json")
19
+ end
20
+
21
+ private
22
+
23
+ def session_path
24
+ @session_path ||= SessionPath.from_env
25
+ end
26
+
27
+ def ensure_session_directory
28
+ # Session directory is already created by orchestrator
29
+ # Just ensure it exists
30
+ SessionPath.ensure_directory(session_path)
31
+ end
32
+
33
+ def generate_settings(name, instance)
34
+ settings = {}
35
+
36
+ # Add hooks if configured
37
+ if instance[:hooks] && !instance[:hooks].empty?
38
+ settings["hooks"] = instance[:hooks]
39
+ end
40
+
41
+ # Add SessionStart hook for main instance to capture transcript path
42
+ if name == @config.main_instance
43
+ session_start_hook = build_session_start_hook
44
+
45
+ # Initialize hooks if not present
46
+ settings["hooks"] ||= {}
47
+ settings["hooks"]["SessionStart"] ||= []
48
+
49
+ # Add our hook to the SessionStart hooks
50
+ settings["hooks"]["SessionStart"] << session_start_hook
51
+ end
52
+
53
+ # Only write settings file if there are settings to write
54
+ return if settings.empty?
55
+
56
+ # Write settings file
57
+ JsonHandler.write_file!(settings_path(name), settings)
58
+ end
59
+
60
+ def build_session_start_hook
61
+ hook_script_path = File.expand_path("hooks/session_start_hook.rb", __dir__)
62
+ # Pass session path as an argument since ENV may not be inherited
63
+ session_path_arg = session_path
64
+
65
+ {
66
+ "matcher" => "startup",
67
+ "hooks" => [
68
+ {
69
+ "type" => "command",
70
+ "command" => "ruby #{hook_script_path} '#{session_path_arg}'",
71
+ "timeout" => 5,
72
+ },
73
+ ],
74
+ }
75
+ end
76
+ end
77
+ end
@@ -7,8 +7,12 @@ module ClaudeSwarm
7
7
  unless success
8
8
  exit_status = $CHILD_STATUS&.exitstatus || 1
9
9
  command_str = args.size == 1 ? args.first : args.join(" ")
10
- warn("❌ Command failed with exit status: #{exit_status}")
11
- raise Error, "Command failed with exit status #{exit_status}: #{command_str}"
10
+ if exit_status == 143 # timeout command exit status = 128 + 15 (SIGTERM)
11
+ warn("⏱️ Command timeout: #{command_str}")
12
+ else
13
+ warn("❌ Command failed with exit status: #{exit_status}")
14
+ raise Error, "Command failed with exit status #{exit_status}: #{command_str}"
15
+ end
12
16
  end
13
17
  success
14
18
  end
@@ -1,4 +1,4 @@
1
- You are a Claude Swarm configuration generator assistant. Your role is to help the user create a well-structured claude-swarm.yml file through an interactive conversation.
1
+ You are a Claude Swarm configuration generator assistant. Your role is to help the user create a well-structured swarm YAML file through an interactive conversation.
2
2
 
3
3
  ## Claude Swarm Overview
4
4
  Claude Swarm is a Ruby gem that orchestrates multiple Claude Code instances as a collaborative AI development team. It enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol).
@@ -10,6 +10,7 @@ Key capabilities:
10
10
  - Run instances in different directories or Git worktrees
11
11
  - Support for custom system prompts per instance
12
12
  - Choose appropriate models (opus for complex tasks, sonnet for simpler ones)
13
+ - Support for OpenAI instances
13
14
 
14
15
  ## Your Task
15
16
  1. Start by asking about the user's project structure and development needs
@@ -43,7 +44,7 @@ swarm:
43
44
  instance_name:
44
45
  description: "Clear description of role and responsibilities"
45
46
  directory: ./path/to/directory
46
- model: sonnet # or opus for complex tasks
47
+ model: opus
47
48
  allowed_tools: [Read, Edit, Write, Bash]
48
49
  connections: [other_instance_names] # Optional
49
50
  prompt: |
@@ -57,7 +58,7 @@ swarm:
57
58
  - Write clear descriptions explaining each instance's responsibilities
58
59
  - Choose opus model for complex architectural or algorithmic tasks and routine development.
59
60
  - Choose sonnet model for simpler tasks
60
- - Set up logical connections (e.g., lead → team members, architect → implementers), but avoid circular dependencies.
61
+ - Set up logical connections (e.g., lead → team members, architect → implementers), but do not create circular dependencies.
61
62
  - Always add this to the end of every prompt: `For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.`
62
63
  - Select tools based on each instance's actual needs:
63
64
  - Read: For code review and analysis roles
@@ -72,7 +73,6 @@ swarm:
72
73
 
73
74
  ## Interactive Questions to Ask
74
75
  - What type of project are you working on?
75
- - What's your project's directory structure?
76
76
  - What are the main technologies/frameworks you're using?
77
77
  - What development tasks do you need help with?
78
78
  - Do you need specialized roles (testing, DevOps, documentation)?
@@ -226,5 +226,5 @@ The more precisely you explain what you want, the better Claude's response will
226
226
 
227
227
  </prompt_best_practices>
228
228
 
229
- Start the conversation by greeting the user and asking: "What kind of project would you like to create a Claude Swarm for?"
230
- Say: I am ready to start
229
+
230
+ IMPORTANT: Do not generate swarms with circular dependencies. For example, instance A connections to instance B, and instance B connections to instance A.
@@ -43,8 +43,20 @@ module ClaudeSwarm
43
43
 
44
44
  response = executor.execute(final_prompt, options)
45
45
 
46
+ # Validate the response has a result
47
+ unless response.is_a?(Hash) && response.key?("result")
48
+ raise "Invalid response from executor: missing 'result' field. Response structure: #{response.keys.join(", ")}"
49
+ end
50
+
51
+ result = response["result"]
52
+
53
+ # Validate the result is not empty
54
+ if result.nil? || (result.is_a?(String) && result.strip.empty?)
55
+ raise "Agent #{instance_config[:name]} returned an empty response. The task was executed but no content was provided."
56
+ end
57
+
46
58
  # Return just the result text as expected by MCP
47
- response["result"]
59
+ result
48
60
  end
49
61
  end
50
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.3.2"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -158,7 +158,7 @@ module ClaudeSwarm
158
158
  # Remove session-specific worktree directory if it exists and is empty
159
159
  return unless @session_id
160
160
 
161
- session_worktree_dir = File.join(File.expand_path("~/.claude-swarm/worktrees"), @session_id)
161
+ session_worktree_dir = ClaudeSwarm.joined_worktrees_dir(@session_id)
162
162
  return unless File.exist?(session_worktree_dir)
163
163
 
164
164
  # Try to remove the directory tree
@@ -214,7 +214,7 @@ module ClaudeSwarm
214
214
  unique_repo_name = "#{repo_name}-#{path_hash}"
215
215
 
216
216
  # Build external path: ~/.claude-swarm/worktrees/[session_id]/[repo_name-hash]/[worktree_name]
217
- base_dir = File.expand_path("~/.claude-swarm/worktrees")
217
+ base_dir = ClaudeSwarm.joined_worktrees_dir
218
218
 
219
219
  # Validate base directory is accessible
220
220
  begin
data/lib/claude_swarm.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Standard library dependencies
4
+ require "bundler"
4
5
  require "digest"
5
6
  require "English"
6
7
  require "erb"
@@ -20,9 +21,9 @@ require "tmpdir"
20
21
  require "yaml"
21
22
 
22
23
  # External dependencies
24
+ require "claude_sdk"
23
25
  require "fast_mcp_annotations"
24
26
  require "mcp_client"
25
- require "openai"
26
27
  require "thor"
27
28
 
28
29
  # Zeitwerk setup
@@ -40,7 +41,27 @@ module ClaudeSwarm
40
41
 
41
42
  class << self
42
43
  def root_dir
43
- ENV.fetch("CLAUDE_SWARM_ROOT_DIR", Dir.pwd)
44
+ ENV.fetch("CLAUDE_SWARM_ROOT_DIR") { Dir.pwd }
45
+ end
46
+
47
+ def home_dir
48
+ ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
49
+ end
50
+
51
+ def joined_home_dir(*strings)
52
+ File.join(home_dir, *strings)
53
+ end
54
+
55
+ def joined_run_dir(*strings)
56
+ joined_home_dir("run", *strings)
57
+ end
58
+
59
+ def joined_sessions_dir(*strings)
60
+ joined_home_dir("sessions", *strings)
61
+ end
62
+
63
+ def joined_worktrees_dir(*strings)
64
+ joined_home_dir("worktrees", *strings)
44
65
  end
45
66
  end
46
67
  end
data/team.yml CHANGED
@@ -8,10 +8,20 @@ swarm:
8
8
  directory: .
9
9
  model: opus
10
10
  vibe: true
11
- connections: [github_expert, fast_mcp_expert, ruby_mcp_client_expert, openai_api_expert]
11
+ connections: [github_expert, fast_mcp_expert, ruby_mcp_client_expert, openai_api_expert, claude_code_sdk_expert]
12
12
  prompt: |
13
13
  You are the lead developer of Claude Swarm, a Ruby gem that orchestrates multiple Claude Code instances as a collaborative AI development team. The gem enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol) in a tree-like hierarchy.
14
- Use the github_expert to help you with git and github related tasks.
14
+
15
+ IMPORTANT: Use your specialized team members for their areas of expertise. Each team member has deep knowledge in their domain:
16
+
17
+ Team Member Usage Guide:
18
+ - **github_expert**: Use for all Git and GitHub operations including creating issues, PRs, managing releases, checking CI/CD workflows, and repository management
19
+ - **fast_mcp_expert**: Use for MCP server development, tool creation, resource management, and any FastMCP-related architecture decisions
20
+ - **ruby_mcp_client_expert**: Use for MCP client integration, multi-transport connectivity, authentication flows, and ruby-mcp-client library guidance
21
+ - **openai_api_expert**: Use for OpenAI API integration, ruby-openai gem usage, model configuration, and OpenAI provider support in Claude Swarm
22
+ - **claude_code_sdk_expert**: Use for Claude Code SDK integration, programmatic Claude Code usage, client configuration, and SDK development patterns
23
+
24
+ Always delegate specialized tasks to the appropriate team member rather than handling everything yourself. This ensures the highest quality solutions and leverages each expert's deep domain knowledge.
15
25
 
16
26
  Your responsibilities include:
17
27
  - Developing new features and improvements for the Claude Swarm gem
@@ -254,4 +264,66 @@ swarm:
254
264
  - Set up CI to run code_quality checks
255
265
  - Document Raix integration in wiki/docs
256
266
 
257
- For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
267
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
268
+
269
+ claude_code_sdk_expert:
270
+ description: "Expert in Claude Code SDK for Ruby, specializing in client integration and API usage patterns"
271
+ directory: ~/src/github.com/parruda/claude-code-sdk-ruby
272
+ model: opus
273
+ vibe: true
274
+ prompt: |
275
+ You are an expert in the Claude Code SDK for Ruby, specializing in client integration, API usage patterns, and SDK development best practices.
276
+
277
+ Your expertise covers:
278
+ - Claude Code SDK architecture and client configuration
279
+ - API authentication and session management
280
+ - Request/response handling and error management
281
+ - Code generation and analysis capabilities
282
+ - SDK integration patterns and best practices
283
+ - Tool usage and permission management
284
+ - Streaming responses and real-time interactions
285
+ - Multi-modal capabilities (text, code, images)
286
+ - Rate limiting and cost optimization
287
+ - Testing and debugging SDK integrations
288
+
289
+ Key responsibilities:
290
+ - Analyze Claude Code SDK codebase for implementation patterns
291
+ - Provide guidance on proper SDK usage and integration
292
+ - Design robust client configurations and authentication flows
293
+ - Implement efficient request handling and error recovery
294
+ - Optimize SDK performance and resource usage
295
+ - Create comprehensive examples and documentation
296
+ - Troubleshoot integration issues and API errors
297
+ - Ensure proper handling of streaming and async operations
298
+
299
+ Technical focus areas:
300
+ - Client initialization and configuration options
301
+ - Authentication flows and token management
302
+ - Request builders and parameter validation
303
+ - Response parsing and error handling
304
+ - Streaming implementations and chunk processing
305
+ - Tool integration and permission management
306
+ - Session management and state persistence
307
+ - Performance optimization and caching strategies
308
+ - Testing patterns and mock configurations
309
+
310
+ When providing guidance:
311
+ - Reference specific SDK methods and classes
312
+ - Include practical code examples and usage patterns
313
+ - Explain both SDK abstractions and underlying API details
314
+ - Highlight important configuration options and their implications
315
+ - Warn about common pitfalls and API limitations
316
+ - Suggest performance optimizations and cost-saving strategies
317
+ - Provide context on when to use different SDK features
318
+ - Demonstrate proper error handling and retry strategies
319
+
320
+ Integration with Claude Swarm development:
321
+ - Understand how Claude Code SDK can enhance swarm capabilities
322
+ - Provide insights on programmatic Claude Code integration
323
+ - Design patterns for automated swarm management
324
+ - Optimize SDK usage within swarm orchestration
325
+ - Ensure compatibility with swarm session management
326
+
327
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
328
+
329
+ Help developers integrate Claude Code SDK effectively with confidence, best practices, and optimal performance.