claude_swarm 0.3.10 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/CLAUDE.md +2 -0
- data/README.md +58 -0
- data/lib/claude_swarm/claude_code_executor.rb +10 -5
- data/lib/claude_swarm/cli.rb +20 -19
- data/lib/claude_swarm/commands/ps.rb +8 -8
- data/lib/claude_swarm/commands/show.rb +4 -5
- data/lib/claude_swarm/configuration.rb +18 -12
- data/lib/claude_swarm/json_handler.rb +91 -0
- data/lib/claude_swarm/mcp_generator.rb +4 -4
- data/lib/claude_swarm/openai/chat_completion.rb +6 -6
- data/lib/claude_swarm/openai/executor.rb +1 -1
- data/lib/claude_swarm/openai/responses.rb +8 -8
- data/lib/claude_swarm/orchestrator.rb +99 -41
- data/lib/claude_swarm/session_cost_calculator.rb +6 -6
- data/lib/claude_swarm/session_path.rb +2 -8
- data/lib/claude_swarm/settings_generator.rb +1 -1
- data/lib/claude_swarm/templates/generation_prompt.md.erb +3 -0
- data/lib/claude_swarm/tools/task_tool.rb +13 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +2 -2
- data/lib/claude_swarm.rb +21 -1
- data/team_v2.yml +367 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be01b40a18b703189532df1e653c15e406c0b798b220c20fe1f014a35331e213
|
4
|
+
data.tar.gz: a4eddd8c10cb1974e9693c27cb9d6660117bd781a4ed16ef52dc1408b4c414d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8207b22b7d3514e910d9f4c72ba7c99e5f5b2f5034e532fa8708ae5d9e9219047153d54ac148c688114cc3f9bd167a4e7a16ceed9439512fe9f1c60c77f8d63
|
7
|
+
data.tar.gz: 53cfc85b8898b8906372e5518913de7c373e98ca2a7de1202f343f1d93f9efc566ed21acee1529f8bc9945e3e60f0cde335040dd49a9d582f723ea739c790c4e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,66 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [1.0.0]
|
4
|
+
|
5
|
+
### Added
|
6
|
+
- **HTTP MCP server configuration support**: Added support for HTTP-type MCP servers alongside existing stdio and SSE types
|
7
|
+
- HTTP servers can be configured with a `url` field
|
8
|
+
- Properly preserves server type (http/sse) in generated configurations
|
9
|
+
- Full compatibility with Claude Code's HTTP MCP server implementation
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- **Updated dependency from fast-mcp-annotations to fast-mcp gem**: Migrated to the consolidated fast-mcp gem (~> 1.6)
|
13
|
+
- Consolidates MCP functionality into a single, more maintainable gem
|
14
|
+
- Maintains all existing functionality with improved performance
|
15
|
+
- **Updated claude-code-sdk-ruby dependency**: Minimum version requirement increased to 0.1.6
|
16
|
+
- Includes latest SDK improvements and bug fixes
|
17
|
+
- Better error handling and stability
|
18
|
+
- **Swarm generation improvements**: Generator now avoids creating swarms with circular dependencies
|
19
|
+
- Prevents generation of invalid configurations
|
20
|
+
- Improves reliability of generated swarm templates
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- **Empty response validation**: Added validation to prevent empty or nil responses from being passed to MCP callers
|
24
|
+
- ClaudeCodeExecutor now validates that Claude SDK returns non-empty result content
|
25
|
+
- TaskTool validates response structure and content before returning to MCP caller
|
26
|
+
- Clear error messages indicate when agent completes execution but provides no response
|
27
|
+
- Prevents silent failures when Claude SDK returns empty results
|
28
|
+
- **Before commands directory handling**: Fixed error when before commands need to create the main instance directory
|
29
|
+
- Smart directory detection: if the main instance directory exists, commands run inside it (for `npm install`, etc.)
|
30
|
+
- If the directory doesn't exist, commands run in the parent directory (allowing `mkdir` commands to create it)
|
31
|
+
- Works correctly with both regular directories and Git worktrees
|
32
|
+
- After commands follow the same logic for consistency
|
33
|
+
- Fixes "No such file or directory @ dir_chdir" errors when before commands create directories
|
34
|
+
|
35
|
+
### Internal
|
36
|
+
- **Centralized JSON handling**: Added JsonHandler class for consistent JSON parsing and generation
|
37
|
+
- Improved error handling for malformed JSON files
|
38
|
+
- Standardized JSON output formatting across all modules
|
39
|
+
- **Centralized CLAUDE_SWARM_HOME handling**: Refactored environment variable management for cleaner code
|
40
|
+
- **Enhanced test coverage**: Added comprehensive configuration tests and enabled branch coverage in SimpleCov
|
41
|
+
- Extensive validation of edge cases including circular dependencies
|
42
|
+
- Better coverage metrics with branch coverage enabled
|
43
|
+
|
44
|
+
## [0.3.11]
|
45
|
+
|
46
|
+
### Added
|
47
|
+
- **Deferred directory validation for before commands**: Directories are now validated after `before` commands run, allowing them to create required directories
|
48
|
+
- Automatically skips initial directory validation when `before` commands are present in configuration
|
49
|
+
- Validates all directories after `before` commands complete successfully
|
50
|
+
- Enables dynamic directory creation workflows without pre-creating directory structures
|
51
|
+
|
52
|
+
### Fixed
|
53
|
+
- **--root-dir parameter path resolution**: Fixed relative config file paths to be resolved relative to the --root-dir value instead of current directory
|
54
|
+
- Config paths are now expanded using the root directory as the base path
|
55
|
+
- Allows running claude-swarm from any location with consistent path resolution
|
56
|
+
- Absolute paths continue to work as expected regardless of --root-dir setting
|
57
|
+
|
58
|
+
### Improved
|
59
|
+
- **Enhanced worktree cleanup on errors**: Improved error handling to ensure worktrees are always cleaned up properly
|
60
|
+
- Added comprehensive error handling with cleanup at all failure points
|
61
|
+
- Worktrees are now cleaned up when worktree setup fails, before commands fail, or directory validation fails
|
62
|
+
- Prevents orphaned worktrees that could clutter the system or cause issues with future runs
|
63
|
+
|
1
64
|
## [0.3.10]
|
2
65
|
|
3
66
|
### Added
|
data/CLAUDE.md
CHANGED
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
6
6
|
|
7
7
|
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).
|
8
8
|
|
9
|
+
SwarmCore is a complete reimagining of Claude Swarm that decouples from Claude Code and runs everything in a single process using RubyLLM for all LLM interactions. It is being developed in `lib/swarm_core`, and using the gemspec swarm-core.gemspec.
|
10
|
+
|
9
11
|
## Development Commands
|
10
12
|
|
11
13
|
### Testing
|
data/README.md
CHANGED
@@ -228,6 +228,53 @@ swarm:
|
|
228
228
|
# Instance definitions...
|
229
229
|
```
|
230
230
|
|
231
|
+
#### YAML Aliases
|
232
|
+
|
233
|
+
Claude Swarm supports [YAML aliases](https://yaml.org/spec/1.2.2/#71-alias-nodes) to reduce duplication in your configuration. This is particularly useful for sharing common values like prompts, tool lists, or MCP configurations across multiple instances:
|
234
|
+
|
235
|
+
```yaml
|
236
|
+
version: 1
|
237
|
+
swarm:
|
238
|
+
name: "Development Team"
|
239
|
+
main: lead
|
240
|
+
instances:
|
241
|
+
lead:
|
242
|
+
description: "Lead developer"
|
243
|
+
prompt: &shared_prompt "You are an expert developer following best practices"
|
244
|
+
allowed_tools: &standard_tools
|
245
|
+
- Read
|
246
|
+
- Edit
|
247
|
+
- Bash
|
248
|
+
- WebSearch
|
249
|
+
mcps: &common_mcps
|
250
|
+
- name: github
|
251
|
+
type: stdio
|
252
|
+
command: gh
|
253
|
+
args: ["mcp"]
|
254
|
+
|
255
|
+
frontend:
|
256
|
+
description: "Frontend developer"
|
257
|
+
prompt: *shared_prompt # Reuses the same prompt
|
258
|
+
allowed_tools: *standard_tools # Reuses the same tool list
|
259
|
+
mcps: *common_mcps # Reuses the same MCP servers
|
260
|
+
directory: ./frontend
|
261
|
+
|
262
|
+
backend:
|
263
|
+
description: "Backend developer"
|
264
|
+
prompt: *shared_prompt # Reuses the same prompt
|
265
|
+
allowed_tools: *standard_tools # Reuses the same tool list
|
266
|
+
mcps: *common_mcps # Reuses the same MCP servers
|
267
|
+
directory: ./backend
|
268
|
+
```
|
269
|
+
|
270
|
+
In this example:
|
271
|
+
- `&shared_prompt` defines an anchor for the prompt string
|
272
|
+
- `&standard_tools` defines an anchor for the tool array
|
273
|
+
- `&common_mcps` defines an anchor for the MCP configuration
|
274
|
+
- `*shared_prompt`, `*standard_tools`, and `*common_mcps` reference those anchors
|
275
|
+
|
276
|
+
This helps maintain consistency across instances and makes configuration updates easier.
|
277
|
+
|
231
278
|
#### Environment Variable Interpolation
|
232
279
|
|
233
280
|
Claude Swarm supports environment variable interpolation in all configuration values using the `${ENV_VAR_NAME}` syntax:
|
@@ -395,6 +442,17 @@ mcps:
|
|
395
442
|
X-Custom-Header: "value"
|
396
443
|
```
|
397
444
|
|
445
|
+
#### http (HTTP-based MCP)
|
446
|
+
```yaml
|
447
|
+
mcps:
|
448
|
+
- name: http_service
|
449
|
+
type: http
|
450
|
+
url: "https://api.example.com/mcp-endpoint"
|
451
|
+
headers: # Optional: custom headers for authentication
|
452
|
+
Authorization: "Bearer ${API_TOKEN}"
|
453
|
+
X-API-Key: "${API_KEY}"
|
454
|
+
```
|
455
|
+
|
398
456
|
### Hooks Configuration
|
399
457
|
|
400
458
|
Claude Swarm supports configuring [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) for each instance. Hooks allow you to run custom scripts before/after tools, on prompt submission, and more. Each instance can have its own hooks configuration.
|
@@ -40,6 +40,11 @@ module ClaudeSwarm
|
|
40
40
|
# Assistant messages only contain content blocks
|
41
41
|
# No need to track for result extraction - result comes from Result message
|
42
42
|
when ClaudeSDK::Messages::Result
|
43
|
+
# Validate that we have actual result content
|
44
|
+
if message.result.nil? || (message.result.is_a?(String) && message.result.strip.empty?)
|
45
|
+
raise ExecutionError, "Claude SDK returned an empty result. The agent completed execution but provided no response content."
|
46
|
+
end
|
47
|
+
|
43
48
|
# Build result response in expected format
|
44
49
|
result_response = {
|
45
50
|
"type" => "result",
|
@@ -91,7 +96,7 @@ module ClaudeSwarm
|
|
91
96
|
updated_at: Time.now.iso8601,
|
92
97
|
}
|
93
98
|
|
94
|
-
|
99
|
+
JsonHandler.write_file!(state_file, state_data)
|
95
100
|
logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
|
96
101
|
rescue StandardError => e
|
97
102
|
logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
|
@@ -114,13 +119,13 @@ module ClaudeSwarm
|
|
114
119
|
end
|
115
120
|
|
116
121
|
def log_system_message(event)
|
117
|
-
logger.debug { "SYSTEM: #{
|
122
|
+
logger.debug { "SYSTEM: #{JsonHandler.pretty_generate!(event)}" }
|
118
123
|
end
|
119
124
|
|
120
125
|
def log_assistant_message(msg)
|
121
126
|
# Assistant messages don't have stop_reason in SDK - they only have content
|
122
127
|
content = msg["content"]
|
123
|
-
logger.debug { "ASSISTANT: #{
|
128
|
+
logger.debug { "ASSISTANT: #{JsonHandler.pretty_generate!(content)}" } if content
|
124
129
|
|
125
130
|
# Log tool calls
|
126
131
|
tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
|
@@ -141,7 +146,7 @@ module ClaudeSwarm
|
|
141
146
|
end
|
142
147
|
|
143
148
|
def log_user_message(content)
|
144
|
-
logger.debug { "USER: #{
|
149
|
+
logger.debug { "USER: #{JsonHandler.pretty_generate!(content)}" }
|
145
150
|
end
|
146
151
|
|
147
152
|
def build_sdk_options(prompt, options)
|
@@ -193,7 +198,7 @@ module ClaudeSwarm
|
|
193
198
|
|
194
199
|
def parse_mcp_config(config_path)
|
195
200
|
# Parse MCP JSON config file and convert to SDK format
|
196
|
-
config =
|
201
|
+
config = JsonHandler.parse_file!(config_path)
|
197
202
|
mcp_servers = {}
|
198
203
|
|
199
204
|
config["mcpServers"]&.each do |name, server_config|
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -42,16 +42,19 @@ module ClaudeSwarm
|
|
42
42
|
type: :string,
|
43
43
|
desc: "Root directory for resolving relative paths (defaults to current directory)"
|
44
44
|
def start(config_file = nil)
|
45
|
+
# Set root directory early so it's available to all components
|
46
|
+
root_dir = options[:root_dir] || Dir.pwd
|
47
|
+
ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
|
48
|
+
|
49
|
+
# Resolve config path relative to root directory
|
45
50
|
config_path = config_file || "claude-swarm.yml"
|
51
|
+
config_path = File.expand_path(config_path, root_dir)
|
52
|
+
|
46
53
|
unless File.exist?(config_path)
|
47
54
|
error("Configuration file not found: #{config_path}")
|
48
55
|
exit(1)
|
49
56
|
end
|
50
57
|
|
51
|
-
# Set root directory early so it's available to all components
|
52
|
-
root_dir = options[:root_dir] || Dir.pwd
|
53
|
-
ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
|
54
|
-
|
55
58
|
say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
|
56
59
|
|
57
60
|
# Validate stream_logs option
|
@@ -331,7 +334,7 @@ module ClaudeSwarm
|
|
331
334
|
system!("command -v claude > /dev/null 2>&1")
|
332
335
|
rescue Error
|
333
336
|
error("Claude CLI is not installed or not in PATH")
|
334
|
-
|
337
|
+
error("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
|
335
338
|
exit(1)
|
336
339
|
end
|
337
340
|
|
@@ -403,12 +406,12 @@ module ClaudeSwarm
|
|
403
406
|
desc: "Number of lines to show initially"
|
404
407
|
def watch(session_id)
|
405
408
|
# Find session path
|
406
|
-
run_symlink =
|
409
|
+
run_symlink = ClaudeSwarm.joined_run_dir(session_id)
|
407
410
|
session_path = if File.symlink?(run_symlink)
|
408
411
|
File.readlink(run_symlink)
|
409
412
|
else
|
410
413
|
# Search in sessions directory
|
411
|
-
Dir.glob(
|
414
|
+
Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
|
412
415
|
File.basename(path) == session_id
|
413
416
|
end
|
414
417
|
end
|
@@ -434,7 +437,7 @@ module ClaudeSwarm
|
|
434
437
|
default: 10,
|
435
438
|
desc: "Maximum number of sessions to display"
|
436
439
|
def list_sessions
|
437
|
-
sessions_dir =
|
440
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
438
441
|
unless Dir.exist?(sessions_dir)
|
439
442
|
say("No sessions found", :yellow)
|
440
443
|
return
|
@@ -508,7 +511,7 @@ module ClaudeSwarm
|
|
508
511
|
private
|
509
512
|
|
510
513
|
def error(message)
|
511
|
-
|
514
|
+
$stderr.puts(Thor::Shell::Color.new.set_color(message, :red))
|
512
515
|
end
|
513
516
|
|
514
517
|
def restore_session(session_id)
|
@@ -559,12 +562,10 @@ module ClaudeSwarm
|
|
559
562
|
# Load session metadata if it exists to check for worktree info
|
560
563
|
session_metadata_file = File.join(session_path, "session_metadata.json")
|
561
564
|
worktree_name = nil
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
|
567
|
-
end
|
565
|
+
metadata = JsonHandler.parse_file(session_metadata_file)
|
566
|
+
if metadata && metadata["worktree"] && metadata["worktree"]["enabled"]
|
567
|
+
worktree_name = metadata["worktree"]["name"]
|
568
|
+
say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
|
568
569
|
end
|
569
570
|
|
570
571
|
# Create orchestrator with restoration mode
|
@@ -589,7 +590,7 @@ module ClaudeSwarm
|
|
589
590
|
end
|
590
591
|
|
591
592
|
def find_session_path(session_id)
|
592
|
-
sessions_dir =
|
593
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
593
594
|
|
594
595
|
# Search for the session ID in all projects
|
595
596
|
Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
|
@@ -601,7 +602,7 @@ module ClaudeSwarm
|
|
601
602
|
end
|
602
603
|
|
603
604
|
def clean_stale_symlinks(days)
|
604
|
-
run_dir =
|
605
|
+
run_dir = ClaudeSwarm.joined_run_dir
|
605
606
|
return 0 unless Dir.exist?(run_dir)
|
606
607
|
|
607
608
|
cleaned = 0
|
@@ -630,10 +631,10 @@ module ClaudeSwarm
|
|
630
631
|
end
|
631
632
|
|
632
633
|
def clean_orphaned_worktrees(days)
|
633
|
-
worktrees_dir =
|
634
|
+
worktrees_dir = ClaudeSwarm.joined_worktrees_dir
|
634
635
|
return 0 unless Dir.exist?(worktrees_dir)
|
635
636
|
|
636
|
-
sessions_dir =
|
637
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
637
638
|
cleaned = 0
|
638
639
|
|
639
640
|
Dir.glob("#{worktrees_dir}/*").each do |session_worktree_dir|
|
@@ -3,10 +3,9 @@
|
|
3
3
|
module ClaudeSwarm
|
4
4
|
module Commands
|
5
5
|
class Ps
|
6
|
-
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
7
|
-
|
8
6
|
def execute
|
9
|
-
|
7
|
+
run_dir = ClaudeSwarm.joined_run_dir
|
8
|
+
unless Dir.exist?(run_dir)
|
10
9
|
puts "No active sessions"
|
11
10
|
return
|
12
11
|
end
|
@@ -14,7 +13,7 @@ module ClaudeSwarm
|
|
14
13
|
sessions = []
|
15
14
|
|
16
15
|
# Read all symlinks in run directory
|
17
|
-
Dir.glob("#{
|
16
|
+
Dir.glob("#{run_dir}/*").each do |symlink|
|
18
17
|
next unless File.symlink?(symlink)
|
19
18
|
|
20
19
|
begin
|
@@ -147,9 +146,10 @@ module ClaudeSwarm
|
|
147
146
|
def get_start_time(session_dir)
|
148
147
|
# Try to get from session metadata first
|
149
148
|
metadata_file = File.join(session_dir, "session_metadata.json")
|
150
|
-
|
151
|
-
|
152
|
-
|
149
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
150
|
+
|
151
|
+
if metadata && metadata["start_time"]
|
152
|
+
return Time.parse(metadata["start_time"])
|
153
153
|
end
|
154
154
|
|
155
155
|
# Fallback to directory creation time
|
@@ -179,7 +179,7 @@ module ClaudeSwarm
|
|
179
179
|
session_metadata_file = File.join(session_dir, "session_metadata.json")
|
180
180
|
return directories unless File.exist?(session_metadata_file)
|
181
181
|
|
182
|
-
metadata =
|
182
|
+
metadata = JsonHandler.parse_file!(session_metadata_file)
|
183
183
|
worktree_info = metadata["worktree"]
|
184
184
|
return directories unless worktree_info && worktree_info["enabled"]
|
185
185
|
|
@@ -63,23 +63,22 @@ module ClaudeSwarm
|
|
63
63
|
|
64
64
|
def find_session_path(session_id)
|
65
65
|
# First check the run directory
|
66
|
-
run_symlink =
|
66
|
+
run_symlink = ClaudeSwarm.joined_run_dir(session_id)
|
67
67
|
if File.symlink?(run_symlink)
|
68
68
|
target = File.readlink(run_symlink)
|
69
69
|
return target if Dir.exist?(target)
|
70
70
|
end
|
71
71
|
|
72
72
|
# Fall back to searching all sessions
|
73
|
-
Dir.glob(
|
73
|
+
Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
|
74
74
|
File.basename(path) == session_id
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
78
|
def get_runtime_info(session_path)
|
79
79
|
metadata_file = File.join(session_path, "session_metadata.json")
|
80
|
-
|
81
|
-
|
82
|
-
metadata = JSON.parse(File.read(metadata_file))
|
80
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
81
|
+
return unless metadata
|
83
82
|
|
84
83
|
if metadata["duration_seconds"]
|
85
84
|
# Session has completed
|
@@ -43,15 +43,30 @@ module ClaudeSwarm
|
|
43
43
|
@swarm["after"] || []
|
44
44
|
end
|
45
45
|
|
46
|
+
def validate_directories
|
47
|
+
@instances.each do |name, instance|
|
48
|
+
# Validate all directories in the directories array
|
49
|
+
instance[:directories].each do |directory|
|
50
|
+
raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
46
55
|
private
|
47
56
|
|
57
|
+
def has_before_commands?
|
58
|
+
@swarm && @swarm["before"] && !@swarm["before"].empty?
|
59
|
+
end
|
60
|
+
|
48
61
|
def load_and_validate
|
49
|
-
@config = YAML.load_file(@config_path)
|
62
|
+
@config = YAML.load_file(@config_path, aliases: true)
|
50
63
|
interpolate_env_vars!(@config)
|
51
64
|
validate_version
|
52
65
|
validate_swarm
|
53
66
|
parse_swarm
|
54
|
-
|
67
|
+
# Skip directory validation if before commands are present
|
68
|
+
# They might create the directories
|
69
|
+
validate_directories unless has_before_commands?
|
55
70
|
rescue Errno::ENOENT
|
56
71
|
raise Error, "Configuration file not found: #{@config_path}"
|
57
72
|
rescue Psych::SyntaxError => e
|
@@ -233,7 +248,7 @@ module ClaudeSwarm
|
|
233
248
|
case mcp["type"]
|
234
249
|
when "stdio"
|
235
250
|
raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
|
236
|
-
when "sse"
|
251
|
+
when "sse", "http"
|
237
252
|
raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
|
238
253
|
else
|
239
254
|
raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
|
@@ -273,15 +288,6 @@ module ClaudeSwarm
|
|
273
288
|
visited.add(instance_name)
|
274
289
|
end
|
275
290
|
|
276
|
-
def validate_directories
|
277
|
-
@instances.each do |name, instance|
|
278
|
-
# Validate all directories in the directories array
|
279
|
-
instance[:directories].each do |directory|
|
280
|
-
raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
|
281
|
-
end
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
291
|
def validate_tool_field(instance_name, config, field_name)
|
286
292
|
return unless config.key?(field_name)
|
287
293
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSwarm
|
4
|
+
# Centralized JSON handling for the Claude Swarm codebase
|
5
|
+
class JsonHandler
|
6
|
+
class << self
|
7
|
+
# Parse JSON string into Ruby object
|
8
|
+
# @param json_string [String] The JSON string to parse
|
9
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
10
|
+
# @return [Object] The parsed Ruby object, or original string if parsing fails and raise_on_error is false
|
11
|
+
# @raise [JSON::ParserError] If the JSON is invalid and raise_on_error is true
|
12
|
+
def parse(json_string, raise_on_error: false)
|
13
|
+
JSON.parse(json_string)
|
14
|
+
rescue JSON::ParserError => e
|
15
|
+
raise e if raise_on_error
|
16
|
+
|
17
|
+
json_string
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse JSON string with exception raising
|
21
|
+
# @param json_string [String] The JSON string to parse
|
22
|
+
# @return [Object] The parsed Ruby object
|
23
|
+
# @raise [JSON::ParserError] If the JSON is invalid
|
24
|
+
def parse!(json_string)
|
25
|
+
parse(json_string, raise_on_error: true)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse JSON from a file with exception raising
|
29
|
+
# @param file_path [String] Path to the JSON file
|
30
|
+
# @return [Object] The parsed Ruby object
|
31
|
+
# @raise [Errno::ENOENT] If the file does not exist
|
32
|
+
# @raise [JSON::ParserError] If the file contains invalid JSON
|
33
|
+
def parse_file!(file_path)
|
34
|
+
content = File.read(file_path)
|
35
|
+
parse!(content)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse JSON from a file, returning nil on error
|
39
|
+
# @param file_path [String] Path to the JSON file
|
40
|
+
# @return [Object, nil] The parsed Ruby object or nil if file doesn't exist or contains invalid JSON
|
41
|
+
def parse_file(file_path)
|
42
|
+
parse_file!(file_path)
|
43
|
+
rescue Errno::ENOENT, JSON::ParserError
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generate pretty-formatted JSON string
|
48
|
+
# @param object [Object] The Ruby object to convert to JSON
|
49
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
50
|
+
# @return [String, nil] The pretty-formatted JSON string, or nil if generation fails and raise_on_error is false
|
51
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON and raise_on_error is true
|
52
|
+
def pretty_generate(object, raise_on_error: false)
|
53
|
+
JSON.pretty_generate(object)
|
54
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
55
|
+
raise e if raise_on_error
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate pretty-formatted JSON string with exception raising
|
61
|
+
# @param object [Object] The Ruby object to convert to JSON
|
62
|
+
# @return [String] The pretty-formatted JSON string
|
63
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
64
|
+
def pretty_generate!(object)
|
65
|
+
pretty_generate(object, raise_on_error: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Write Ruby object to a JSON file with pretty formatting
|
69
|
+
# @param file_path [String] Path to the JSON file
|
70
|
+
# @param object [Object] The Ruby object to write
|
71
|
+
# @return [Boolean] True if successful, false if generation or write fails
|
72
|
+
def write_file(file_path, object)
|
73
|
+
json_string = pretty_generate!(object)
|
74
|
+
File.write(file_path, json_string)
|
75
|
+
true
|
76
|
+
rescue JSON::GeneratorError, JSON::NestingError, SystemCallError
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write Ruby object to a JSON file with exception raising
|
81
|
+
# @param file_path [String] Path to the JSON file
|
82
|
+
# @param object [Object] The Ruby object to write
|
83
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
84
|
+
# @raise [SystemCallError] If the file cannot be written
|
85
|
+
def write_file!(file_path, object)
|
86
|
+
json_string = pretty_generate!(object)
|
87
|
+
File.write(file_path, json_string)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -73,7 +73,7 @@ module ClaudeSwarm
|
|
73
73
|
"mcpServers" => mcp_servers,
|
74
74
|
}
|
75
75
|
|
76
|
-
|
76
|
+
JsonHandler.write_file!(mcp_config_path(name), config)
|
77
77
|
end
|
78
78
|
|
79
79
|
def build_mcp_server_config(mcp)
|
@@ -86,9 +86,9 @@ module ClaudeSwarm
|
|
86
86
|
}.tap do |config|
|
87
87
|
config["env"] = mcp["env"] if mcp["env"]
|
88
88
|
end
|
89
|
-
when "sse"
|
89
|
+
when "sse", "http"
|
90
90
|
{
|
91
|
-
"type" => "
|
91
|
+
"type" => mcp["type"],
|
92
92
|
"url" => mcp["url"],
|
93
93
|
}.tap do |config|
|
94
94
|
config["headers"] = mcp["headers"] if mcp["headers"]
|
@@ -220,7 +220,7 @@ module ClaudeSwarm
|
|
220
220
|
return unless Dir.exist?(state_dir)
|
221
221
|
|
222
222
|
Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
|
223
|
-
data =
|
223
|
+
data = JsonHandler.parse_file!(state_file)
|
224
224
|
instance_name = data["instance_name"]
|
225
225
|
instance_id = data["instance_id"]
|
226
226
|
|
@@ -83,7 +83,7 @@ module ClaudeSwarm
|
|
83
83
|
parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
|
84
84
|
|
85
85
|
# Log the request parameters
|
86
|
-
@executor.logger.info { "Chat API Request (depth=#{depth}): #{
|
86
|
+
@executor.logger.info { "Chat API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
|
87
87
|
|
88
88
|
# Append to session JSON
|
89
89
|
append_to_session_json({
|
@@ -98,7 +98,7 @@ module ClaudeSwarm
|
|
98
98
|
response = @openai_client.chat(parameters: parameters)
|
99
99
|
rescue StandardError => e
|
100
100
|
@executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
|
101
|
-
@executor.logger.error { "Request parameters: #{
|
101
|
+
@executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
|
102
102
|
|
103
103
|
# Try to extract and log the response body for better debugging
|
104
104
|
if e.respond_to?(:response)
|
@@ -127,7 +127,7 @@ module ClaudeSwarm
|
|
127
127
|
end
|
128
128
|
|
129
129
|
# Log the response
|
130
|
-
@executor.logger.info { "Chat API Response (depth=#{depth}): #{
|
130
|
+
@executor.logger.info { "Chat API Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
|
131
131
|
|
132
132
|
# Append to session JSON
|
133
133
|
append_to_session_json({
|
@@ -169,7 +169,7 @@ module ClaudeSwarm
|
|
169
169
|
|
170
170
|
def execute_and_append_tool_results(tool_calls, messages)
|
171
171
|
# Log tool calls
|
172
|
-
@executor.logger.info { "Executing tool calls: #{
|
172
|
+
@executor.logger.info { "Executing tool calls: #{JsonHandler.pretty_generate!(tool_calls)}" }
|
173
173
|
|
174
174
|
# Append to session JSON
|
175
175
|
append_to_session_json({
|
@@ -186,10 +186,10 @@ module ClaudeSwarm
|
|
186
186
|
|
187
187
|
begin
|
188
188
|
# Parse arguments
|
189
|
-
tool_args = tool_args_str.is_a?(String) ?
|
189
|
+
tool_args = tool_args_str.is_a?(String) ? JsonHandler.parse!(tool_args_str) : tool_args_str
|
190
190
|
|
191
191
|
# Log tool execution
|
192
|
-
@executor.logger.info { "Executing tool: #{tool_name} with args: #{
|
192
|
+
@executor.logger.info { "Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
|
193
193
|
|
194
194
|
# Execute tool via MCP
|
195
195
|
result = @mcp_client.call_tool(tool_name, tool_args)
|
@@ -122,7 +122,7 @@ module ClaudeSwarm
|
|
122
122
|
return unless @mcp_config && File.exist?(@mcp_config)
|
123
123
|
|
124
124
|
# Read MCP config to find MCP servers
|
125
|
-
mcp_data =
|
125
|
+
mcp_data = JsonHandler.parse_file!(@mcp_config)
|
126
126
|
|
127
127
|
# Build MCP configurations from servers
|
128
128
|
mcp_configs = build_mcp_configs(mcp_data["mcpServers"])
|