claude_swarm 0.1.11 ā 0.1.13
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 +47 -0
- data/README.md +85 -15
- data/claude-swarm.yml +42 -0
- data/example/session-restoration-demo.yml +19 -0
- data/lib/claude_swarm/claude_code_executor.rb +101 -20
- data/lib/claude_swarm/claude_mcp_server.rb +16 -4
- data/lib/claude_swarm/cli.rb +169 -5
- data/lib/claude_swarm/configuration.rb +33 -7
- data/lib/claude_swarm/mcp_generator.rb +62 -22
- data/lib/claude_swarm/orchestrator.rb +122 -24
- data/lib/claude_swarm/permission_mcp_server.rb +14 -15
- data/lib/claude_swarm/process_tracker.rb +78 -0
- data/lib/claude_swarm/session_path.rb +50 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +8 -0
- metadata +5 -1
data/lib/claude_swarm/cli.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "json"
|
4
5
|
require_relative "configuration"
|
5
6
|
require_relative "mcp_generator"
|
6
7
|
require_relative "orchestrator"
|
@@ -24,7 +25,15 @@ module ClaudeSwarm
|
|
24
25
|
desc: "Stream session logs to stdout (only works with -p)"
|
25
26
|
method_option :debug, type: :boolean, default: false,
|
26
27
|
desc: "Enable debug output"
|
28
|
+
method_option :session_id, type: :string,
|
29
|
+
desc: "Resume a previous session by ID or path"
|
27
30
|
def start(config_file = nil)
|
31
|
+
# Handle session restoration
|
32
|
+
if options[:session_id]
|
33
|
+
restore_session(options[:session_id])
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
28
37
|
config_path = config_file || options[:config]
|
29
38
|
unless File.exist?(config_path)
|
30
39
|
error "Configuration file not found: #{config_path}"
|
@@ -41,12 +50,10 @@ module ClaudeSwarm
|
|
41
50
|
|
42
51
|
begin
|
43
52
|
config = Configuration.new(config_path)
|
44
|
-
|
45
|
-
generator = McpGenerator.new(config, vibe: options[:vibe], timestamp: session_timestamp)
|
53
|
+
generator = McpGenerator.new(config, vibe: options[:vibe])
|
46
54
|
orchestrator = Orchestrator.new(config, generator,
|
47
55
|
vibe: options[:vibe],
|
48
56
|
prompt: options[:prompt],
|
49
|
-
session_timestamp: session_timestamp,
|
50
57
|
stream_logs: options[:stream_logs],
|
51
58
|
debug: options[:debug])
|
52
59
|
orchestrator.start
|
@@ -83,6 +90,12 @@ module ClaudeSwarm
|
|
83
90
|
desc: "Run with --dangerously-skip-permissions"
|
84
91
|
method_option :calling_instance, type: :string, required: true,
|
85
92
|
desc: "Name of the instance that launched this MCP server"
|
93
|
+
method_option :calling_instance_id, type: :string,
|
94
|
+
desc: "Unique ID of the instance that launched this MCP server"
|
95
|
+
method_option :instance_id, type: :string,
|
96
|
+
desc: "Unique ID of this instance"
|
97
|
+
method_option :claude_session_id, type: :string,
|
98
|
+
desc: "Claude session ID to resume"
|
86
99
|
def mcp_serve
|
87
100
|
instance_config = {
|
88
101
|
name: options[:name],
|
@@ -93,11 +106,17 @@ module ClaudeSwarm
|
|
93
106
|
tools: options[:tools] || [],
|
94
107
|
disallowed_tools: options[:disallowed_tools] || [],
|
95
108
|
mcp_config_path: options[:mcp_config_path],
|
96
|
-
vibe: options[:vibe]
|
109
|
+
vibe: options[:vibe],
|
110
|
+
instance_id: options[:instance_id],
|
111
|
+
claude_session_id: options[:claude_session_id]
|
97
112
|
}
|
98
113
|
|
99
114
|
begin
|
100
|
-
server = ClaudeMcpServer.new(
|
115
|
+
server = ClaudeMcpServer.new(
|
116
|
+
instance_config,
|
117
|
+
calling_instance: options[:calling_instance],
|
118
|
+
calling_instance_id: options[:calling_instance_id]
|
119
|
+
)
|
101
120
|
server.start
|
102
121
|
rescue StandardError => e
|
103
122
|
error "Error starting MCP server: #{e.message}"
|
@@ -173,6 +192,79 @@ module ClaudeSwarm
|
|
173
192
|
say "Claude Swarm #{VERSION}"
|
174
193
|
end
|
175
194
|
|
195
|
+
desc "list-sessions", "List all available Claude Swarm sessions"
|
196
|
+
method_option :limit, aliases: "-l", type: :numeric, default: 10,
|
197
|
+
desc: "Maximum number of sessions to display"
|
198
|
+
def list_sessions
|
199
|
+
sessions_dir = File.expand_path("~/.claude-swarm/sessions")
|
200
|
+
unless Dir.exist?(sessions_dir)
|
201
|
+
say "No sessions found", :yellow
|
202
|
+
return
|
203
|
+
end
|
204
|
+
|
205
|
+
# Find all sessions with MCP configs
|
206
|
+
sessions = []
|
207
|
+
Dir.glob("#{sessions_dir}/*/*/*.mcp.json").each do |mcp_path|
|
208
|
+
session_path = File.dirname(mcp_path)
|
209
|
+
session_id = File.basename(session_path)
|
210
|
+
project_name = File.basename(File.dirname(session_path))
|
211
|
+
|
212
|
+
# Skip if we've already processed this session
|
213
|
+
next if sessions.any? { |s| s[:path] == session_path }
|
214
|
+
|
215
|
+
# Try to load session info
|
216
|
+
config_file = File.join(session_path, "config.yml")
|
217
|
+
next unless File.exist?(config_file)
|
218
|
+
|
219
|
+
# Load the config to get swarm info
|
220
|
+
config_data = YAML.load_file(config_file)
|
221
|
+
swarm_name = config_data.dig("swarm", "name") || "Unknown"
|
222
|
+
main_instance = config_data.dig("swarm", "main") || "Unknown"
|
223
|
+
|
224
|
+
mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
|
225
|
+
|
226
|
+
# Get creation time from directory
|
227
|
+
created_at = File.stat(session_path).ctime
|
228
|
+
|
229
|
+
sessions << {
|
230
|
+
path: session_path,
|
231
|
+
id: session_id,
|
232
|
+
project: project_name,
|
233
|
+
created_at: created_at,
|
234
|
+
main_instance: main_instance,
|
235
|
+
instances_count: mcp_files.size,
|
236
|
+
swarm_name: swarm_name,
|
237
|
+
config_path: config_file
|
238
|
+
}
|
239
|
+
rescue StandardError
|
240
|
+
# Skip invalid manifests
|
241
|
+
next
|
242
|
+
end
|
243
|
+
|
244
|
+
if sessions.empty?
|
245
|
+
say "No sessions found", :yellow
|
246
|
+
return
|
247
|
+
end
|
248
|
+
|
249
|
+
# Sort by creation time (newest first)
|
250
|
+
sessions.sort_by! { |s| -s[:created_at].to_i }
|
251
|
+
sessions = sessions.first(options[:limit])
|
252
|
+
|
253
|
+
# Display sessions
|
254
|
+
say "\nAvailable sessions (newest first):\n", :bold
|
255
|
+
sessions.each do |session|
|
256
|
+
say "\n#{session[:project]}/#{session[:id]}", :green
|
257
|
+
say " Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}"
|
258
|
+
say " Main: #{session[:main_instance]}"
|
259
|
+
say " Instances: #{session[:instances_count]}"
|
260
|
+
say " Swarm: #{session[:swarm_name]}"
|
261
|
+
say " Config: #{session[:config_path]}", :cyan
|
262
|
+
end
|
263
|
+
|
264
|
+
say "\nTo resume a session, run:", :bold
|
265
|
+
say " claude-swarm --session-id <session-id>", :cyan
|
266
|
+
end
|
267
|
+
|
176
268
|
desc "tools-mcp", "Start a permission management MCP server for tool access control"
|
177
269
|
method_option :allowed_tools, aliases: "-t", type: :string,
|
178
270
|
desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
|
@@ -196,5 +288,77 @@ module ClaudeSwarm
|
|
196
288
|
def error(message)
|
197
289
|
say message, :red
|
198
290
|
end
|
291
|
+
|
292
|
+
def restore_session(session_id)
|
293
|
+
say "Restoring session: #{session_id}", :green
|
294
|
+
|
295
|
+
# Find the session path
|
296
|
+
session_path = find_session_path(session_id)
|
297
|
+
unless session_path
|
298
|
+
error "Session not found: #{session_id}"
|
299
|
+
exit 1
|
300
|
+
end
|
301
|
+
|
302
|
+
begin
|
303
|
+
# Load session info from instance ID in MCP config
|
304
|
+
mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
|
305
|
+
if mcp_files.empty?
|
306
|
+
error "No MCP configuration files found in session"
|
307
|
+
exit 1
|
308
|
+
end
|
309
|
+
|
310
|
+
# Load the configuration from the session directory
|
311
|
+
config_file = File.join(session_path, "config.yml")
|
312
|
+
|
313
|
+
unless File.exist?(config_file)
|
314
|
+
error "Configuration file not found in session"
|
315
|
+
exit 1
|
316
|
+
end
|
317
|
+
|
318
|
+
# Change to the original start directory if it exists
|
319
|
+
start_dir_file = File.join(session_path, "start_directory")
|
320
|
+
if File.exist?(start_dir_file)
|
321
|
+
original_dir = File.read(start_dir_file).strip
|
322
|
+
if Dir.exist?(original_dir)
|
323
|
+
Dir.chdir(original_dir)
|
324
|
+
say "Changed to original directory: #{original_dir}", :green unless options[:prompt]
|
325
|
+
else
|
326
|
+
error "Original directory no longer exists: #{original_dir}"
|
327
|
+
exit 1
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
config = Configuration.new(config_file, base_dir: Dir.pwd)
|
332
|
+
|
333
|
+
# Create orchestrator with restoration mode
|
334
|
+
generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
|
335
|
+
orchestrator = Orchestrator.new(config, generator,
|
336
|
+
vibe: options[:vibe],
|
337
|
+
prompt: options[:prompt],
|
338
|
+
stream_logs: options[:stream_logs],
|
339
|
+
debug: options[:debug],
|
340
|
+
restore_session_path: session_path)
|
341
|
+
orchestrator.start
|
342
|
+
rescue StandardError => e
|
343
|
+
error "Failed to restore session: #{e.message}"
|
344
|
+
error e.backtrace.join("\n") if options[:debug]
|
345
|
+
exit 1
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def find_session_path(session_id)
|
350
|
+
sessions_dir = File.expand_path("~/.claude-swarm/sessions")
|
351
|
+
|
352
|
+
# Check if it's a full path
|
353
|
+
return session_id if File.exist?(File.join(session_id, "config.yml"))
|
354
|
+
|
355
|
+
# Search for the session ID in all projects
|
356
|
+
Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
|
357
|
+
config_path = File.join(path, "config.yml")
|
358
|
+
return path if File.exist?(config_path)
|
359
|
+
end
|
360
|
+
|
361
|
+
nil
|
362
|
+
end
|
199
363
|
end
|
200
364
|
end
|
@@ -5,11 +5,11 @@ require "pathname"
|
|
5
5
|
|
6
6
|
module ClaudeSwarm
|
7
7
|
class Configuration
|
8
|
-
attr_reader :config, :swarm_name, :main_instance, :instances
|
8
|
+
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
|
9
9
|
|
10
|
-
def initialize(config_path)
|
10
|
+
def initialize(config_path, base_dir: nil)
|
11
11
|
@config_path = Pathname.new(config_path).expand_path
|
12
|
-
@config_dir = @config_path.dirname
|
12
|
+
@config_dir = base_dir || @config_path.dirname
|
13
13
|
load_and_validate
|
14
14
|
end
|
15
15
|
|
@@ -60,14 +60,15 @@ module ClaudeSwarm
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def parse_swarm
|
63
|
-
swarm = @config["swarm"]
|
64
|
-
@swarm_name = swarm["name"]
|
65
|
-
@main_instance = swarm["main"]
|
63
|
+
@swarm = @config["swarm"]
|
64
|
+
@swarm_name = @swarm["name"]
|
65
|
+
@main_instance = @swarm["main"]
|
66
66
|
@instances = {}
|
67
|
-
swarm["instances"].each do |name, config|
|
67
|
+
@swarm["instances"].each do |name, config|
|
68
68
|
@instances[name] = parse_instance(name, config)
|
69
69
|
end
|
70
70
|
validate_connections
|
71
|
+
detect_circular_dependencies
|
71
72
|
end
|
72
73
|
|
73
74
|
def parse_instance(name, config)
|
@@ -127,6 +128,31 @@ module ClaudeSwarm
|
|
127
128
|
end
|
128
129
|
end
|
129
130
|
|
131
|
+
def detect_circular_dependencies
|
132
|
+
@instances.each_key do |instance_name|
|
133
|
+
visited = Set.new
|
134
|
+
path = []
|
135
|
+
detect_cycle_from(instance_name, visited, path)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def detect_cycle_from(instance_name, visited, path)
|
140
|
+
return if visited.include?(instance_name)
|
141
|
+
|
142
|
+
if path.include?(instance_name)
|
143
|
+
cycle_start = path.index(instance_name)
|
144
|
+
cycle = path[cycle_start..] + [instance_name]
|
145
|
+
raise Error, "Circular dependency detected: #{cycle.join(" -> ")}"
|
146
|
+
end
|
147
|
+
|
148
|
+
path.push(instance_name)
|
149
|
+
@instances[instance_name][:connections].each do |connection|
|
150
|
+
detect_cycle_from(connection, visited, path)
|
151
|
+
end
|
152
|
+
path.pop
|
153
|
+
visited.add(instance_name)
|
154
|
+
end
|
155
|
+
|
130
156
|
def validate_directories
|
131
157
|
@instances.each do |name, instance|
|
132
158
|
directory = instance[:directory]
|
@@ -3,45 +3,52 @@
|
|
3
3
|
require "json"
|
4
4
|
require "fileutils"
|
5
5
|
require "shellwords"
|
6
|
+
require "securerandom"
|
7
|
+
require_relative "session_path"
|
6
8
|
|
7
9
|
module ClaudeSwarm
|
8
10
|
class McpGenerator
|
9
|
-
|
10
|
-
SESSIONS_SUBDIR = "sessions"
|
11
|
-
|
12
|
-
def initialize(configuration, vibe: false, timestamp: nil)
|
11
|
+
def initialize(configuration, vibe: false, restore_session_path: nil)
|
13
12
|
@config = configuration
|
14
13
|
@vibe = vibe
|
15
|
-
@
|
14
|
+
@restore_session_path = restore_session_path
|
15
|
+
@session_path = nil # Will be set when needed
|
16
|
+
@instance_ids = {} # Store instance IDs for all instances
|
17
|
+
@restore_states = {} # Store loaded state data during restoration
|
16
18
|
end
|
17
19
|
|
18
20
|
def generate_all
|
19
21
|
ensure_swarm_directory
|
20
22
|
|
23
|
+
if @restore_session_path
|
24
|
+
# Load existing instance IDs and states from state files
|
25
|
+
load_instance_states
|
26
|
+
else
|
27
|
+
# Generate new instance IDs
|
28
|
+
@config.instances.each_key do |name|
|
29
|
+
@instance_ids[name] = "#{name}_#{SecureRandom.hex(4)}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
21
33
|
@config.instances.each do |name, instance|
|
22
34
|
generate_mcp_config(name, instance)
|
23
35
|
end
|
24
36
|
end
|
25
37
|
|
26
38
|
def mcp_config_path(instance_name)
|
27
|
-
File.join(
|
39
|
+
File.join(session_path, "#{instance_name}.mcp.json")
|
28
40
|
end
|
29
41
|
|
30
42
|
private
|
31
43
|
|
32
|
-
def
|
33
|
-
|
44
|
+
def session_path
|
45
|
+
@session_path ||= SessionPath.from_env
|
34
46
|
end
|
35
47
|
|
36
48
|
def ensure_swarm_directory
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
session_dir = File.join(swarm_dir, SESSIONS_SUBDIR, @timestamp)
|
41
|
-
FileUtils.mkdir_p(session_dir)
|
42
|
-
|
43
|
-
gitignore_path = File.join(swarm_dir, ".gitignore")
|
44
|
-
File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
|
49
|
+
# Session directory is already created by orchestrator
|
50
|
+
# Just ensure it exists
|
51
|
+
SessionPath.ensure_directory(session_path)
|
45
52
|
end
|
46
53
|
|
47
54
|
def generate_mcp_config(name, instance)
|
@@ -55,13 +62,18 @@ module ClaudeSwarm
|
|
55
62
|
# Add connection MCPs for other instances
|
56
63
|
instance[:connections].each do |connection_name|
|
57
64
|
connected_instance = @config.instances[connection_name]
|
58
|
-
mcp_servers[connection_name] = build_instance_mcp_config(
|
65
|
+
mcp_servers[connection_name] = build_instance_mcp_config(
|
66
|
+
connection_name, connected_instance,
|
67
|
+
calling_instance: name, calling_instance_id: @instance_ids[name]
|
68
|
+
)
|
59
69
|
end
|
60
70
|
|
61
71
|
# Add permission MCP server if not in vibe mode (global or instance-specific)
|
62
72
|
mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools], instance[:disallowed_tools]) unless @vibe || instance[:vibe]
|
63
73
|
|
64
74
|
config = {
|
75
|
+
"instance_id" => @instance_ids[name],
|
76
|
+
"instance_name" => name,
|
65
77
|
"mcpServers" => mcp_servers
|
66
78
|
}
|
67
79
|
|
@@ -86,7 +98,7 @@ module ClaudeSwarm
|
|
86
98
|
end
|
87
99
|
end
|
88
100
|
|
89
|
-
def build_instance_mcp_config(name, instance, calling_instance:)
|
101
|
+
def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
|
90
102
|
# Get the path to the claude-swarm executable
|
91
103
|
exe_path = "claude-swarm"
|
92
104
|
|
@@ -111,8 +123,18 @@ module ClaudeSwarm
|
|
111
123
|
|
112
124
|
args.push("--calling-instance", calling_instance) if calling_instance
|
113
125
|
|
126
|
+
args.push("--calling-instance-id", calling_instance_id) if calling_instance_id
|
127
|
+
|
128
|
+
args.push("--instance-id", @instance_ids[name]) if @instance_ids[name]
|
129
|
+
|
114
130
|
args.push("--vibe") if @vibe || instance[:vibe]
|
115
131
|
|
132
|
+
# Add claude session ID if restoring
|
133
|
+
if @restore_states[name.to_s]
|
134
|
+
claude_session_id = @restore_states[name.to_s]["claude_session_id"]
|
135
|
+
args.push("--claude-session-id", claude_session_id) if claude_session_id
|
136
|
+
end
|
137
|
+
|
116
138
|
{
|
117
139
|
"type" => "stdio",
|
118
140
|
"command" => exe_path,
|
@@ -120,6 +142,27 @@ module ClaudeSwarm
|
|
120
142
|
}
|
121
143
|
end
|
122
144
|
|
145
|
+
def load_instance_states
|
146
|
+
state_dir = File.join(@restore_session_path, "state")
|
147
|
+
return unless Dir.exist?(state_dir)
|
148
|
+
|
149
|
+
Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
|
150
|
+
data = JSON.parse(File.read(state_file))
|
151
|
+
instance_name = data["instance_name"]
|
152
|
+
instance_id = data["instance_id"]
|
153
|
+
|
154
|
+
# Check both string and symbol keys since config instances might have either
|
155
|
+
if instance_name && (@config.instances.key?(instance_name) || @config.instances.key?(instance_name.to_sym))
|
156
|
+
# Store with the same key type as in @config.instances
|
157
|
+
key = @config.instances.key?(instance_name) ? instance_name : instance_name.to_sym
|
158
|
+
@instance_ids[key] = instance_id
|
159
|
+
@restore_states[instance_name] = data
|
160
|
+
end
|
161
|
+
rescue StandardError
|
162
|
+
# Skip invalid state files
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
123
166
|
def build_permission_mcp_config(allowed_tools, disallowed_tools)
|
124
167
|
exe_path = "claude-swarm"
|
125
168
|
|
@@ -134,10 +177,7 @@ module ClaudeSwarm
|
|
134
177
|
{
|
135
178
|
"type" => "stdio",
|
136
179
|
"command" => exe_path,
|
137
|
-
"args" => args
|
138
|
-
"env" => {
|
139
|
-
"CLAUDE_SWARM_SESSION_TIMESTAMP" => @timestamp
|
140
|
-
}
|
180
|
+
"args" => args
|
141
181
|
}
|
142
182
|
end
|
143
183
|
end
|
@@ -1,38 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "shellwords"
|
4
|
+
require "json"
|
5
|
+
require "fileutils"
|
6
|
+
require_relative "session_path"
|
7
|
+
require_relative "process_tracker"
|
4
8
|
|
5
9
|
module ClaudeSwarm
|
6
10
|
class Orchestrator
|
7
|
-
def initialize(configuration, mcp_generator, vibe: false, prompt: nil,
|
11
|
+
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
|
12
|
+
restore_session_path: nil)
|
8
13
|
@config = configuration
|
9
14
|
@generator = mcp_generator
|
10
15
|
@vibe = vibe
|
11
16
|
@prompt = prompt
|
12
|
-
@session_timestamp = session_timestamp || Time.now.strftime("%Y%m%d_%H%M%S")
|
13
17
|
@stream_logs = stream_logs
|
14
18
|
@debug = debug
|
19
|
+
@restore_session_path = restore_session_path
|
15
20
|
end
|
16
21
|
|
17
22
|
def start
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
if @restore_session_path
|
24
|
+
unless @prompt
|
25
|
+
puts "š Restoring Claude Swarm: #{@config.swarm_name}"
|
26
|
+
puts "š Vibe mode ON" if @vibe
|
27
|
+
puts
|
28
|
+
end
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
puts
|
29
|
-
end
|
30
|
+
# Use existing session path
|
31
|
+
session_path = @restore_session_path
|
32
|
+
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
33
|
+
ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
unless @prompt
|
36
|
+
puts "š Using existing session: #{session_path}/"
|
37
|
+
puts
|
38
|
+
end
|
39
|
+
|
40
|
+
# Initialize process tracker
|
41
|
+
@process_tracker = ProcessTracker.new(session_path)
|
42
|
+
|
43
|
+
# Set up signal handlers to clean up child processes
|
44
|
+
setup_signal_handlers
|
45
|
+
|
46
|
+
# Regenerate MCP configurations with session IDs for restoration
|
47
|
+
@generator.generate_all
|
48
|
+
unless @prompt
|
49
|
+
puts "ā Regenerated MCP configurations with session IDs"
|
50
|
+
puts
|
51
|
+
end
|
52
|
+
else
|
53
|
+
unless @prompt
|
54
|
+
puts "š Starting Claude Swarm: #{@config.swarm_name}"
|
55
|
+
puts "š Vibe mode ON" if @vibe
|
56
|
+
puts
|
57
|
+
end
|
58
|
+
|
59
|
+
# Generate and set session path for all instances
|
60
|
+
session_path = SessionPath.generate(working_dir: Dir.pwd)
|
61
|
+
SessionPath.ensure_directory(session_path)
|
62
|
+
|
63
|
+
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
64
|
+
ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
|
65
|
+
|
66
|
+
unless @prompt
|
67
|
+
puts "š Session files will be saved to: #{session_path}/"
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
|
71
|
+
# Initialize process tracker
|
72
|
+
@process_tracker = ProcessTracker.new(session_path)
|
73
|
+
|
74
|
+
# Set up signal handlers to clean up child processes
|
75
|
+
setup_signal_handlers
|
76
|
+
|
77
|
+
# Generate all MCP configuration files
|
78
|
+
@generator.generate_all
|
79
|
+
unless @prompt
|
80
|
+
puts "ā Generated MCP configurations in session directory"
|
81
|
+
puts
|
82
|
+
end
|
83
|
+
|
84
|
+
# Save swarm config path for restoration
|
85
|
+
save_swarm_config_path(session_path)
|
36
86
|
end
|
37
87
|
|
38
88
|
# Launch the main instance
|
@@ -64,19 +114,47 @@ module ClaudeSwarm
|
|
64
114
|
end
|
65
115
|
|
66
116
|
# Clean up log streaming thread
|
67
|
-
|
117
|
+
if log_thread
|
118
|
+
log_thread.terminate
|
119
|
+
log_thread.join
|
120
|
+
end
|
68
121
|
|
69
|
-
|
70
|
-
|
122
|
+
# Clean up child processes
|
123
|
+
cleanup_processes
|
71
124
|
end
|
72
125
|
|
73
126
|
private
|
74
127
|
|
128
|
+
def save_swarm_config_path(session_path)
|
129
|
+
# Copy the YAML config file to the session directory
|
130
|
+
config_copy_path = File.join(session_path, "config.yml")
|
131
|
+
FileUtils.cp(@config.config_path, config_copy_path)
|
132
|
+
|
133
|
+
# Save the original working directory
|
134
|
+
start_dir_file = File.join(session_path, "start_directory")
|
135
|
+
File.write(start_dir_file, Dir.pwd)
|
136
|
+
end
|
137
|
+
|
138
|
+
def setup_signal_handlers
|
139
|
+
%w[INT TERM QUIT].each do |signal|
|
140
|
+
Signal.trap(signal) do
|
141
|
+
puts "\nš Received #{signal} signal, cleaning up..."
|
142
|
+
cleanup_processes
|
143
|
+
exit
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def cleanup_processes
|
149
|
+
@process_tracker.cleanup_all
|
150
|
+
puts "ā Cleanup complete"
|
151
|
+
rescue StandardError => e
|
152
|
+
puts "ā ļø Error during cleanup: #{e.message}"
|
153
|
+
end
|
154
|
+
|
75
155
|
def start_log_streaming
|
76
156
|
Thread.new do
|
77
|
-
session_log_path = File.join(
|
78
|
-
ClaudeSwarm::ClaudeCodeExecutor::SESSIONS_DIR,
|
79
|
-
@session_timestamp, "session.log")
|
157
|
+
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
80
158
|
|
81
159
|
# Wait for log file to be created
|
82
160
|
sleep 0.1 until File.exist?(session_log_path)
|
@@ -107,6 +185,26 @@ module ClaudeSwarm
|
|
107
185
|
instance[:model]
|
108
186
|
]
|
109
187
|
|
188
|
+
# Add resume flag if restoring session
|
189
|
+
if @restore_session_path
|
190
|
+
# Look for main instance state file
|
191
|
+
main_instance_name = @config.main_instance
|
192
|
+
state_files = Dir.glob(File.join(@restore_session_path, "state", "*.json"))
|
193
|
+
|
194
|
+
# Find the state file for the main instance
|
195
|
+
state_files.each do |state_file|
|
196
|
+
state_data = JSON.parse(File.read(state_file))
|
197
|
+
next unless state_data["instance_name"] == main_instance_name
|
198
|
+
|
199
|
+
claude_session_id = state_data["claude_session_id"]
|
200
|
+
if claude_session_id
|
201
|
+
parts << "--resume"
|
202
|
+
parts << claude_session_id
|
203
|
+
end
|
204
|
+
break
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
110
208
|
if @vibe || instance[:vibe]
|
111
209
|
parts << "--dangerously-skip-permissions"
|
112
210
|
else
|
@@ -5,13 +5,11 @@ require "fast_mcp"
|
|
5
5
|
require "logger"
|
6
6
|
require "fileutils"
|
7
7
|
require_relative "permission_tool"
|
8
|
+
require_relative "session_path"
|
9
|
+
require_relative "process_tracker"
|
8
10
|
|
9
11
|
module ClaudeSwarm
|
10
12
|
class PermissionMcpServer
|
11
|
-
# Directory constants
|
12
|
-
SWARM_DIR = ".claude-swarm"
|
13
|
-
SESSIONS_DIR = "sessions"
|
14
|
-
|
15
13
|
# Server configuration
|
16
14
|
SERVER_NAME = "claude-swarm-permissions"
|
17
15
|
SERVER_VERSION = "1.0.0"
|
@@ -49,6 +47,13 @@ module ClaudeSwarm
|
|
49
47
|
end
|
50
48
|
|
51
49
|
def create_and_start_server
|
50
|
+
# Track this process
|
51
|
+
session_path = SessionPath.from_env
|
52
|
+
if session_path && File.exist?(session_path)
|
53
|
+
tracker = ProcessTracker.new(session_path)
|
54
|
+
tracker.track_pid(Process.pid, "mcp_permissions")
|
55
|
+
end
|
56
|
+
|
52
57
|
server = FastMcp::Server.new(
|
53
58
|
name: SERVER_NAME,
|
54
59
|
version: SERVER_VERSION
|
@@ -60,20 +65,14 @@ module ClaudeSwarm
|
|
60
65
|
end
|
61
66
|
|
62
67
|
def setup_logging
|
63
|
-
|
64
|
-
|
68
|
+
session_path = SessionPath.from_env
|
69
|
+
SessionPath.ensure_directory(session_path)
|
70
|
+
@logger = create_logger(session_path)
|
65
71
|
@logger.info("Permission MCP server logging initialized")
|
66
72
|
end
|
67
73
|
|
68
|
-
def
|
69
|
-
|
70
|
-
session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, session_timestamp)
|
71
|
-
FileUtils.mkdir_p(session_dir)
|
72
|
-
session_dir
|
73
|
-
end
|
74
|
-
|
75
|
-
def create_logger(session_dir)
|
76
|
-
log_path = File.join(session_dir, "permissions.log")
|
74
|
+
def create_logger(session_path)
|
75
|
+
log_path = File.join(session_path, "permissions.log")
|
77
76
|
logger = Logger.new(log_path)
|
78
77
|
logger.level = Logger::DEBUG
|
79
78
|
logger.formatter = log_formatter
|