claude_swarm 0.1.19 → 0.2.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/.rubocop.yml +9 -63
- data/.rubocop_todo.yml +11 -0
- data/CHANGELOG.md +110 -0
- data/CLAUDE.md +64 -2
- data/README.md +190 -28
- data/Rakefile +1 -1
- data/examples/mixed-provider-swarm.yml +23 -0
- data/examples/monitoring-demo.yml +4 -4
- data/lib/claude_swarm/claude_code_executor.rb +7 -13
- data/lib/claude_swarm/claude_mcp_server.rb +26 -17
- data/lib/claude_swarm/cli.rb +384 -265
- data/lib/claude_swarm/commands/ps.rb +22 -24
- data/lib/claude_swarm/commands/show.rb +45 -63
- data/lib/claude_swarm/configuration.rb +137 -8
- data/lib/claude_swarm/mcp_generator.rb +39 -15
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +301 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +221 -45
- data/lib/claude_swarm/process_tracker.rb +7 -7
- data/lib/claude_swarm/session_cost_calculator.rb +93 -0
- data/lib/claude_swarm/session_path.rb +3 -5
- data/lib/claude_swarm/system_utils.rb +16 -0
- data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
- data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
- data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
- data/lib/claude_swarm/tools/task_tool.rb +43 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +145 -48
- data/lib/claude_swarm.rb +34 -12
- data/llms.txt +2 -2
- data/single.yml +482 -6
- data/team.yml +344 -0
- metadata +65 -14
- data/claude-swarm.yml +0 -64
- data/lib/claude_swarm/reset_session_tool.rb +0 -22
- data/lib/claude_swarm/session_info_tool.rb +0 -22
- data/lib/claude_swarm/task_tool.rb +0 -39
- /data/{example → examples}/claude-swarm.yml +0 -0
- /data/{example → examples}/microservices-team.yml +0 -0
- /data/{example → examples}/session-restoration-demo.yml +0 -0
- /data/{example → examples}/test-generation.yml +0 -0
@@ -1,19 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "English"
|
4
|
-
require "shellwords"
|
5
|
-
require "json"
|
6
|
-
require "fileutils"
|
7
|
-
require_relative "session_path"
|
8
|
-
require_relative "process_tracker"
|
9
|
-
require_relative "worktree_manager"
|
10
|
-
|
11
3
|
module ClaudeSwarm
|
12
4
|
class Orchestrator
|
5
|
+
include SystemUtils
|
13
6
|
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
14
7
|
|
15
8
|
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
|
16
|
-
|
9
|
+
restore_session_path: nil, worktree: nil, session_id: nil)
|
17
10
|
@config = configuration
|
18
11
|
@generator = mcp_generator
|
19
12
|
@vibe = vibe
|
@@ -22,18 +15,24 @@ module ClaudeSwarm
|
|
22
15
|
@debug = debug
|
23
16
|
@restore_session_path = restore_session_path
|
24
17
|
@session_path = nil
|
18
|
+
@provided_session_id = session_id
|
25
19
|
# Store worktree option for later use
|
26
20
|
@worktree_option = worktree
|
27
21
|
@needs_worktree_manager = worktree.is_a?(String) || worktree == "" ||
|
28
|
-
|
22
|
+
configuration.instances.values.any? { |inst| !inst[:worktree].nil? }
|
29
23
|
# Store modified instances after worktree setup
|
30
24
|
@modified_instances = nil
|
25
|
+
# Track start time for runtime calculation
|
26
|
+
@start_time = nil
|
31
27
|
|
32
28
|
# Set environment variable for prompt mode to suppress output
|
33
29
|
ENV["CLAUDE_SWARM_PROMPT"] = "1" if @prompt
|
34
30
|
end
|
35
31
|
|
36
32
|
def start
|
33
|
+
# Track start time
|
34
|
+
@start_time = Time.now
|
35
|
+
|
37
36
|
if @restore_session_path
|
38
37
|
unless @prompt
|
39
38
|
puts "🔄 Restoring Claude Swarm: #{@config.swarm_name}"
|
@@ -78,7 +77,11 @@ module ClaudeSwarm
|
|
78
77
|
end
|
79
78
|
|
80
79
|
# Generate and set session path for all instances
|
81
|
-
session_path =
|
80
|
+
session_path = if @provided_session_id
|
81
|
+
SessionPath.generate(working_dir: Dir.pwd, session_id: @provided_session_id)
|
82
|
+
else
|
83
|
+
SessionPath.generate(working_dir: Dir.pwd)
|
84
|
+
end
|
82
85
|
SessionPath.ensure_directory(session_path)
|
83
86
|
@session_path = session_path
|
84
87
|
|
@@ -131,29 +134,6 @@ module ClaudeSwarm
|
|
131
134
|
save_swarm_config_path(session_path)
|
132
135
|
end
|
133
136
|
|
134
|
-
# Execute before commands if specified
|
135
|
-
before_commands = @config.before_commands
|
136
|
-
if before_commands.any? && !@restore_session_path
|
137
|
-
unless @prompt
|
138
|
-
puts "⚙️ Executing before commands..."
|
139
|
-
puts
|
140
|
-
end
|
141
|
-
|
142
|
-
success = execute_before_commands(before_commands)
|
143
|
-
unless success
|
144
|
-
puts "❌ Before commands failed. Aborting swarm launch." unless @prompt
|
145
|
-
cleanup_processes
|
146
|
-
cleanup_run_symlink
|
147
|
-
cleanup_worktrees
|
148
|
-
exit 1
|
149
|
-
end
|
150
|
-
|
151
|
-
unless @prompt
|
152
|
-
puts "✓ Before commands completed successfully"
|
153
|
-
puts
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
137
|
# Launch the main instance (fetch after worktree setup to get modified paths)
|
158
138
|
main_instance = @config.main_instance_config
|
159
139
|
unless @prompt
|
@@ -174,7 +154,7 @@ module ClaudeSwarm
|
|
174
154
|
|
175
155
|
command = build_main_command(main_instance)
|
176
156
|
if @debug && !@prompt
|
177
|
-
puts "Running: #{command}"
|
157
|
+
puts "🏃 Running: #{format_command_for_display(command)}"
|
178
158
|
puts
|
179
159
|
end
|
180
160
|
|
@@ -182,9 +162,36 @@ module ClaudeSwarm
|
|
182
162
|
log_thread = nil
|
183
163
|
log_thread = start_log_streaming if @prompt && @stream_logs
|
184
164
|
|
165
|
+
# Write the current process PID (orchestrator) to a file for easy access
|
166
|
+
main_pid_file = File.join(@session_path, "main_pid")
|
167
|
+
File.write(main_pid_file, Process.pid.to_s)
|
168
|
+
|
185
169
|
# Execute the main instance - this will cascade to other instances via MCP
|
186
170
|
Dir.chdir(main_instance[:directory]) do
|
187
|
-
|
171
|
+
# Execute before commands if specified
|
172
|
+
before_commands = @config.before_commands
|
173
|
+
if before_commands.any? && !@restore_session_path
|
174
|
+
unless @prompt
|
175
|
+
puts "⚙️ Executing before commands..."
|
176
|
+
puts
|
177
|
+
end
|
178
|
+
|
179
|
+
success = execute_before_commands?(before_commands)
|
180
|
+
unless success
|
181
|
+
puts "❌ Before commands failed. Aborting swarm launch." unless @prompt
|
182
|
+
cleanup_processes
|
183
|
+
cleanup_run_symlink
|
184
|
+
cleanup_worktrees
|
185
|
+
exit(1)
|
186
|
+
end
|
187
|
+
|
188
|
+
unless @prompt
|
189
|
+
puts "✓ Before commands completed successfully"
|
190
|
+
puts
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
system!(*command)
|
188
195
|
end
|
189
196
|
|
190
197
|
# Clean up log streaming thread
|
@@ -193,6 +200,27 @@ module ClaudeSwarm
|
|
193
200
|
log_thread.join
|
194
201
|
end
|
195
202
|
|
203
|
+
# Display runtime and cost summary
|
204
|
+
display_summary
|
205
|
+
|
206
|
+
# Execute after commands if specified
|
207
|
+
after_commands = @config.after_commands
|
208
|
+
if after_commands.any? && !@restore_session_path
|
209
|
+
Dir.chdir(main_instance[:directory]) do
|
210
|
+
unless @prompt
|
211
|
+
puts
|
212
|
+
puts "⚙️ Executing after commands..."
|
213
|
+
puts
|
214
|
+
end
|
215
|
+
|
216
|
+
success = execute_after_commands?(after_commands)
|
217
|
+
if !success && !@prompt
|
218
|
+
puts "⚠️ Some after commands failed"
|
219
|
+
puts
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
196
224
|
# Clean up child processes and run symlink
|
197
225
|
cleanup_processes
|
198
226
|
cleanup_run_symlink
|
@@ -201,7 +229,7 @@ module ClaudeSwarm
|
|
201
229
|
|
202
230
|
private
|
203
231
|
|
204
|
-
def execute_before_commands(commands)
|
232
|
+
def execute_before_commands?(commands)
|
205
233
|
log_file = File.join(@session_path, "session.log") if @session_path
|
206
234
|
|
207
235
|
commands.each_with_index do |command, index|
|
@@ -217,7 +245,7 @@ module ClaudeSwarm
|
|
217
245
|
puts "Debug: Executing command #{index + 1}/#{commands.size}: #{command}" if @debug && !@prompt
|
218
246
|
|
219
247
|
# Use system with output capture
|
220
|
-
output =
|
248
|
+
output = %x(#{command} 2>&1)
|
221
249
|
success = $CHILD_STATUS.success?
|
222
250
|
|
223
251
|
# Log the output
|
@@ -256,6 +284,62 @@ module ClaudeSwarm
|
|
256
284
|
true
|
257
285
|
end
|
258
286
|
|
287
|
+
def execute_after_commands?(commands)
|
288
|
+
log_file = File.join(@session_path, "session.log") if @session_path
|
289
|
+
all_succeeded = true
|
290
|
+
|
291
|
+
commands.each_with_index do |command, index|
|
292
|
+
# Log the command execution to session log
|
293
|
+
if @session_path
|
294
|
+
File.open(log_file, "a") do |f|
|
295
|
+
f.puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Executing after command #{index + 1}/#{commands.size}: #{command}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Execute the command and capture output
|
300
|
+
begin
|
301
|
+
puts "Debug: Executing after command #{index + 1}/#{commands.size}: #{command}" if @debug && !@prompt
|
302
|
+
|
303
|
+
# Use system with output capture
|
304
|
+
output = %x(#{command} 2>&1)
|
305
|
+
success = $CHILD_STATUS.success?
|
306
|
+
|
307
|
+
# Log the output
|
308
|
+
if @session_path
|
309
|
+
File.open(log_file, "a") do |f|
|
310
|
+
f.puts "Command output:"
|
311
|
+
f.puts output
|
312
|
+
f.puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
313
|
+
f.puts "-" * 80
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Show output if in debug mode or if command failed
|
318
|
+
if (@debug || !success) && !@prompt
|
319
|
+
puts "After command #{index + 1} output:"
|
320
|
+
puts output
|
321
|
+
puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
322
|
+
end
|
323
|
+
|
324
|
+
unless success
|
325
|
+
puts "❌ After command #{index + 1} failed: #{command}" unless @prompt
|
326
|
+
all_succeeded = false
|
327
|
+
end
|
328
|
+
rescue StandardError => e
|
329
|
+
puts "Error executing after command #{index + 1}: #{e.message}" unless @prompt
|
330
|
+
if @session_path
|
331
|
+
File.open(log_file, "a") do |f|
|
332
|
+
f.puts "Error: #{e.message}"
|
333
|
+
f.puts "-" * 80
|
334
|
+
end
|
335
|
+
end
|
336
|
+
all_succeeded = false
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
all_succeeded
|
341
|
+
end
|
342
|
+
|
259
343
|
def save_swarm_config_path(session_path)
|
260
344
|
# Copy the YAML config file to the session directory
|
261
345
|
config_copy_path = File.join(session_path, "config.yml")
|
@@ -269,8 +353,9 @@ module ClaudeSwarm
|
|
269
353
|
metadata = {
|
270
354
|
"start_directory" => Dir.pwd,
|
271
355
|
"timestamp" => Time.now.utc.iso8601,
|
356
|
+
"start_time" => @start_time.utc.iso8601,
|
272
357
|
"swarm_name" => @config.swarm_name,
|
273
|
-
"claude_swarm_version" => VERSION
|
358
|
+
"claude_swarm_version" => VERSION,
|
274
359
|
}
|
275
360
|
|
276
361
|
# Add worktree info if applicable
|
@@ -281,9 +366,23 @@ module ClaudeSwarm
|
|
281
366
|
end
|
282
367
|
|
283
368
|
def setup_signal_handlers
|
284
|
-
|
369
|
+
["INT", "TERM", "QUIT"].each do |signal|
|
285
370
|
Signal.trap(signal) do
|
286
371
|
puts "\n🛑 Received #{signal} signal, cleaning up..."
|
372
|
+
display_summary
|
373
|
+
|
374
|
+
# Execute after commands if configured
|
375
|
+
main_instance = @config.main_instance_config
|
376
|
+
after_commands = @config.after_commands
|
377
|
+
if after_commands.any? && !@restore_session_path && !@prompt
|
378
|
+
Dir.chdir(main_instance[:directory]) do
|
379
|
+
puts
|
380
|
+
puts "⚙️ Executing after commands..."
|
381
|
+
puts
|
382
|
+
execute_after_commands?(after_commands)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
287
386
|
cleanup_processes
|
288
387
|
cleanup_run_symlink
|
289
388
|
cleanup_worktrees
|
@@ -307,6 +406,71 @@ module ClaudeSwarm
|
|
307
406
|
puts "⚠️ Error during worktree cleanup: #{e.message}"
|
308
407
|
end
|
309
408
|
|
409
|
+
def display_summary
|
410
|
+
return unless @session_path && @start_time
|
411
|
+
|
412
|
+
end_time = Time.now
|
413
|
+
runtime_seconds = (end_time - @start_time).to_i
|
414
|
+
|
415
|
+
# Update session metadata with end time
|
416
|
+
update_session_end_time(end_time)
|
417
|
+
|
418
|
+
# Calculate total cost from session logs
|
419
|
+
total_cost = calculate_total_cost
|
420
|
+
|
421
|
+
puts
|
422
|
+
puts "=" * 50
|
423
|
+
puts "🏁 Claude Swarm Summary"
|
424
|
+
puts "=" * 50
|
425
|
+
puts "Runtime: #{format_duration(runtime_seconds)}"
|
426
|
+
puts "Total Cost: #{format_cost(total_cost)}"
|
427
|
+
puts "Session: #{File.basename(@session_path)}"
|
428
|
+
puts "=" * 50
|
429
|
+
end
|
430
|
+
|
431
|
+
def update_session_end_time(end_time)
|
432
|
+
metadata_file = File.join(@session_path, "session_metadata.json")
|
433
|
+
return unless File.exist?(metadata_file)
|
434
|
+
|
435
|
+
metadata = JSON.parse(File.read(metadata_file))
|
436
|
+
metadata["end_time"] = end_time.utc.iso8601
|
437
|
+
metadata["duration_seconds"] = (end_time - @start_time).to_i
|
438
|
+
|
439
|
+
File.write(metadata_file, JSON.pretty_generate(metadata))
|
440
|
+
rescue StandardError => e
|
441
|
+
puts "⚠️ Error updating session metadata: #{e.message}" unless @prompt
|
442
|
+
end
|
443
|
+
|
444
|
+
def calculate_total_cost
|
445
|
+
log_file = File.join(@session_path, "session.log.json")
|
446
|
+
result = SessionCostCalculator.calculate_total_cost(log_file)
|
447
|
+
|
448
|
+
# Check if main instance has cost data
|
449
|
+
main_instance_name = @config.main_instance
|
450
|
+
@main_has_cost = result[:instances_with_cost].include?(main_instance_name)
|
451
|
+
|
452
|
+
result[:total_cost]
|
453
|
+
end
|
454
|
+
|
455
|
+
def format_duration(seconds)
|
456
|
+
hours = seconds / 3600
|
457
|
+
minutes = (seconds % 3600) / 60
|
458
|
+
secs = seconds % 60
|
459
|
+
|
460
|
+
parts = []
|
461
|
+
parts << "#{hours}h" if hours.positive?
|
462
|
+
parts << "#{minutes}m" if minutes.positive?
|
463
|
+
parts << "#{secs}s"
|
464
|
+
|
465
|
+
parts.join(" ")
|
466
|
+
end
|
467
|
+
|
468
|
+
def format_cost(cost)
|
469
|
+
cost_str = format("$%.4f", cost)
|
470
|
+
cost_str += " (excluding main instance)" unless @main_has_cost
|
471
|
+
cost_str
|
472
|
+
end
|
473
|
+
|
310
474
|
def create_run_symlink
|
311
475
|
return unless @session_path
|
312
476
|
|
@@ -341,7 +505,7 @@ module ClaudeSwarm
|
|
341
505
|
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
342
506
|
|
343
507
|
# Wait for log file to be created
|
344
|
-
sleep
|
508
|
+
sleep(0.1) until File.exist?(session_log_path)
|
345
509
|
|
346
510
|
# Open file and seek to end
|
347
511
|
File.open(session_log_path, "r") do |file|
|
@@ -350,10 +514,10 @@ module ClaudeSwarm
|
|
350
514
|
loop do
|
351
515
|
changes = file.read
|
352
516
|
if changes
|
353
|
-
print
|
517
|
+
print(changes)
|
354
518
|
$stdout.flush
|
355
519
|
else
|
356
|
-
sleep
|
520
|
+
sleep(0.1)
|
357
521
|
end
|
358
522
|
end
|
359
523
|
end
|
@@ -362,11 +526,21 @@ module ClaudeSwarm
|
|
362
526
|
end
|
363
527
|
end
|
364
528
|
|
529
|
+
def format_command_for_display(command)
|
530
|
+
command.map do |part|
|
531
|
+
if part.match?(/\s|'|"/)
|
532
|
+
"'#{part.gsub("'", "'\\\\''")}'"
|
533
|
+
else
|
534
|
+
part
|
535
|
+
end
|
536
|
+
end.join(" ")
|
537
|
+
end
|
538
|
+
|
365
539
|
def build_main_command(instance)
|
366
540
|
parts = [
|
367
541
|
"claude",
|
368
542
|
"--model",
|
369
|
-
instance[:model]
|
543
|
+
instance[:model],
|
370
544
|
]
|
371
545
|
|
372
546
|
# Add resume flag if restoring session
|
@@ -456,7 +630,9 @@ module ClaudeSwarm
|
|
456
630
|
end
|
457
631
|
|
458
632
|
# Restore worktrees using the saved configuration
|
459
|
-
|
633
|
+
# Extract session ID from the session path
|
634
|
+
session_id = File.basename(session_path)
|
635
|
+
@worktree_manager = WorktreeManager.new(worktree_data["shared_name"], session_id: session_id)
|
460
636
|
|
461
637
|
# Get all instances and restore their worktree paths
|
462
638
|
all_instances = @config.instances.values
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fileutils"
|
4
|
-
|
5
3
|
module ClaudeSwarm
|
6
4
|
class ProcessTracker
|
7
5
|
PIDS_DIR = "pids"
|
@@ -39,7 +37,7 @@ module ClaudeSwarm
|
|
39
37
|
puts "✓ Terminated MCP server: #{name} (PID: #{pid})"
|
40
38
|
|
41
39
|
# Give it a moment to terminate gracefully
|
42
|
-
sleep
|
40
|
+
sleep(0.1)
|
43
41
|
|
44
42
|
# Force kill if still running
|
45
43
|
begin
|
@@ -62,11 +60,13 @@ module ClaudeSwarm
|
|
62
60
|
FileUtils.rm_rf(@pids_dir)
|
63
61
|
end
|
64
62
|
|
65
|
-
|
66
|
-
|
63
|
+
class << self
|
64
|
+
def cleanup_session(session_path)
|
65
|
+
return unless Dir.exist?(File.join(session_path, PIDS_DIR))
|
67
66
|
|
68
|
-
|
69
|
-
|
67
|
+
tracker = new(session_path)
|
68
|
+
tracker.cleanup_all
|
69
|
+
end
|
70
70
|
end
|
71
71
|
|
72
72
|
private
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSwarm
|
4
|
+
module SessionCostCalculator
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Calculate total cost from session log file
|
8
|
+
# Returns a hash with:
|
9
|
+
# - total_cost: Total cost in USD
|
10
|
+
# - instances_with_cost: Set of instance names that have cost data
|
11
|
+
def calculate_total_cost(session_log_path)
|
12
|
+
return { total_cost: 0.0, instances_with_cost: Set.new } unless File.exist?(session_log_path)
|
13
|
+
|
14
|
+
total_cost = 0.0
|
15
|
+
instances_with_cost = Set.new
|
16
|
+
|
17
|
+
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"]
|
22
|
+
end
|
23
|
+
rescue JSON::ParserError
|
24
|
+
next
|
25
|
+
end
|
26
|
+
|
27
|
+
{
|
28
|
+
total_cost: total_cost,
|
29
|
+
instances_with_cost: instances_with_cost,
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calculate simple total cost (for backward compatibility)
|
34
|
+
def calculate_simple_total(session_log_path)
|
35
|
+
calculate_total_cost(session_log_path)[:total_cost]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse instance hierarchy with costs from session log
|
39
|
+
# Returns a hash of instances with their cost data and relationships
|
40
|
+
def parse_instance_hierarchy(session_log_path)
|
41
|
+
instances = {}
|
42
|
+
|
43
|
+
return instances unless File.exist?(session_log_path)
|
44
|
+
|
45
|
+
File.foreach(session_log_path) do |line|
|
46
|
+
data = JSON.parse(line)
|
47
|
+
instance_name = data["instance"]
|
48
|
+
instance_id = data["instance_id"]
|
49
|
+
calling_instance = data["calling_instance"]
|
50
|
+
|
51
|
+
# Initialize instance data
|
52
|
+
instances[instance_name] ||= {
|
53
|
+
name: instance_name,
|
54
|
+
id: instance_id,
|
55
|
+
cost: 0.0,
|
56
|
+
calls: 0,
|
57
|
+
called_by: Set.new,
|
58
|
+
calls_to: Set.new,
|
59
|
+
has_cost_data: false,
|
60
|
+
}
|
61
|
+
|
62
|
+
# Track relationships
|
63
|
+
if calling_instance && calling_instance != instance_name
|
64
|
+
instances[instance_name][:called_by] << calling_instance
|
65
|
+
|
66
|
+
instances[calling_instance] ||= {
|
67
|
+
name: calling_instance,
|
68
|
+
id: data["calling_instance_id"],
|
69
|
+
cost: 0.0,
|
70
|
+
calls: 0,
|
71
|
+
called_by: Set.new,
|
72
|
+
calls_to: Set.new,
|
73
|
+
has_cost_data: false,
|
74
|
+
}
|
75
|
+
instances[calling_instance][:calls_to] << instance_name
|
76
|
+
end
|
77
|
+
|
78
|
+
# Track costs and calls
|
79
|
+
if data.dig("event", "type") == "result"
|
80
|
+
instances[instance_name][:calls] += 1
|
81
|
+
if (cost = data.dig("event", "total_cost_usd"))
|
82
|
+
instances[instance_name][:cost] += cost
|
83
|
+
instances[instance_name][:has_cost_data] = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue JSON::ParserError
|
87
|
+
next
|
88
|
+
end
|
89
|
+
|
90
|
+
instances
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fileutils"
|
4
|
-
|
5
3
|
module ClaudeSwarm
|
6
4
|
module SessionPath
|
7
5
|
SESSIONS_DIR = "sessions"
|
@@ -26,10 +24,10 @@ module ClaudeSwarm
|
|
26
24
|
path.gsub(%r{[/\\]}, "+")
|
27
25
|
end
|
28
26
|
|
29
|
-
# Generate a full session path for a given directory and
|
30
|
-
def generate(working_dir: Dir.pwd,
|
27
|
+
# Generate a full session path for a given directory and session ID
|
28
|
+
def generate(working_dir: Dir.pwd, session_id: SecureRandom.uuid)
|
31
29
|
project_name = project_folder_name(working_dir)
|
32
|
-
File.join(swarm_home, SESSIONS_DIR, project_name,
|
30
|
+
File.join(swarm_home, SESSIONS_DIR, project_name, session_id)
|
33
31
|
end
|
34
32
|
|
35
33
|
# Ensure the session directory exists
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSwarm
|
4
|
+
module SystemUtils
|
5
|
+
def system!(*args)
|
6
|
+
success = system(*args)
|
7
|
+
unless success
|
8
|
+
exit_status = $CHILD_STATUS&.exitstatus || 1
|
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}"
|
12
|
+
end
|
13
|
+
success
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|