claude_swarm 0.1.12 → 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 +28 -0
- data/README.md +84 -14
- data/example/session-restoration-demo.yml +19 -0
- data/lib/claude_swarm/claude_code_executor.rb +67 -8
- data/lib/claude_swarm/claude_mcp_server.rb +15 -3
- data/lib/claude_swarm/cli.rb +168 -2
- data/lib/claude_swarm/configuration.rb +7 -7
- data/lib/claude_swarm/mcp_generator.rb +53 -3
- data/lib/claude_swarm/orchestrator.rb +118 -23
- data/lib/claude_swarm/permission_mcp_server.rb +8 -0
- data/lib/claude_swarm/process_tracker.rb +78 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +8 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57e09297e72fe54ef127b5f7b5789c9e9d825279404ffa96c29f2ff0e7fbc622
|
4
|
+
data.tar.gz: 6e1d719d97d2f8fefa4ec8d0e9396e5fba867012b94d88d915247aea18690523
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ad9a411c8bc503829927ea534fed74e9f525dd62d68fc6eb9978667afdb40435b9bac0c13320c097f1f41556a8d50215c723c8066dec7366cf7d847d07a7e9e
|
7
|
+
data.tar.gz: 490824249ac673b7904f05a084d2ff0190a1034529a9302f423d00e965cd7c793b519b1d8e318a36a1e58af80706cb1fda80dd507af525b45f7df26299abd3f9
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- **Session restoration support (Experimental)**: Session management with the ability to resume previous Claude Swarm sessions. Note: This is an experimental feature with limitations - the main instance's conversation context is not fully restored
|
5
|
+
- New `--session-id` flag to resume a session by ID or path
|
6
|
+
- New `list-sessions` command to view available sessions with metadata
|
7
|
+
- Automatic capture and persistence of Claude session IDs for all instances
|
8
|
+
- Individual instance states stored in `state/` directory with instance ID as filename (e.g., `state/lead_abc123.json`)
|
9
|
+
- Swarm configuration copied to session directory as `config.yml` for restoration
|
10
|
+
- **Instance ID tracking**: Each instance now gets a unique ID in the format `instance_name_<hex>` for better identification in logs
|
11
|
+
- **Enhanced logging with instance IDs**: All log messages now include instance IDs when available (e.g., `lead (lead_1234abcd) -> backend (backend_5678efgh)`)
|
12
|
+
- **Calling instance ID propagation**: When one instance calls another, both the calling instance name and ID are passed for complete tracking
|
13
|
+
- Instance IDs are stored in MCP configuration files with `instance_id` and `instance_name` fields
|
14
|
+
- New CLI options: `--instance-id` and `--calling-instance-id` for the `mcp-serve` command
|
15
|
+
- ClaudeCodeExecutor now tracks and logs both instance and calling instance IDs
|
16
|
+
- **Process tracking and cleanup**: Added automatic tracking and cleanup of child MCP server processes
|
17
|
+
- New `ProcessTracker` class creates individual PID files in a `pids/` directory within the session path
|
18
|
+
- Signal handlers (INT, TERM, QUIT) ensure all child processes are terminated when the main instance exits
|
19
|
+
- Prevents orphaned MCP server processes from continuing to run after swarm termination
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- Human-readable logs improved to show instance IDs in parentheses after instance names for easier tracking of multi-instance interactions
|
23
|
+
- `log_request` method enhanced to include instance IDs in structured JSON logs
|
24
|
+
- Configuration class now accepts optional `base_dir` parameter to support session restoration from different directories
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
- Fixed issue where child MCP server processes would continue running after the main instance exits
|
28
|
+
|
1
29
|
## [0.1.12]
|
2
30
|
### Added
|
3
31
|
- **Circular dependency detection**: Configuration validation now detects and reports circular dependencies between instances
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Claude Swarm
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/claude_swarm)
|
4
4
|
[](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml)
|
5
5
|
|
6
6
|
Claude Swarm 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) in a tree-like hierarchy. Define your swarm topology in simple YAML and let Claude instances delegate tasks through connected instances. Perfect for complex projects requiring specialized AI agents for frontend, backend, testing, DevOps, or research tasks.
|
@@ -80,7 +80,7 @@ claude-swarm --vibe # That will allow ALL tools for all instances! Be Careful!
|
|
80
80
|
This will:
|
81
81
|
- Launch the main instance (lead) with connections to other instances
|
82
82
|
- The lead instance can communicate with the other instances via MCP
|
83
|
-
- All session files are stored in
|
83
|
+
- All session files are stored in `~/.claude-swarm/sessions/{project}/{timestamp}/` (customizable via `CLAUDE_SWARM_HOME`)
|
84
84
|
|
85
85
|
#### Multi-Level Swarm Example
|
86
86
|
|
@@ -446,6 +446,14 @@ claude-swarm --vibe
|
|
446
446
|
claude-swarm -p "Implement the new user authentication feature"
|
447
447
|
claude-swarm --prompt "Fix the bug in the payment module"
|
448
448
|
|
449
|
+
# Resume a previous session by ID
|
450
|
+
claude-swarm --session-id 20241206_143022
|
451
|
+
claude-swarm --session-id ~/path/to/session
|
452
|
+
|
453
|
+
# List available sessions
|
454
|
+
claude-swarm list-sessions
|
455
|
+
claude-swarm list-sessions --limit 20
|
456
|
+
|
449
457
|
# Show version
|
450
458
|
claude-swarm version
|
451
459
|
|
@@ -457,6 +465,70 @@ claude-swarm tools-mcp --allowed-tools 'Read,Edit' --disallowed-tools 'Edit(*.lo
|
|
457
465
|
claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TIMESTAMP
|
458
466
|
```
|
459
467
|
|
468
|
+
### Session Management and Restoration (Experimental)
|
469
|
+
|
470
|
+
Claude Swarm provides experimental session management with restoration capabilities. **Note: This feature is experimental and has limitations - the main instance's conversation context is not fully restored.**
|
471
|
+
|
472
|
+
#### Session Structure
|
473
|
+
All session files are organized in `~/.claude-swarm/sessions/{project}/{timestamp}/`:
|
474
|
+
- `config.yml`: Copy of the original swarm configuration
|
475
|
+
- `state/`: Directory containing individual instance states
|
476
|
+
- `{instance_id}.json`: Claude session ID and status for each instance (e.g., `lead_abc123.json`)
|
477
|
+
- `{instance_name}.mcp.json`: MCP configuration files
|
478
|
+
- `session.log`: Human-readable request/response tracking
|
479
|
+
- `session.log.json`: All events in JSONL format (one JSON per line)
|
480
|
+
- `permissions.log`: Permission checks and decisions
|
481
|
+
|
482
|
+
#### Listing Sessions
|
483
|
+
View your previous Claude Swarm sessions:
|
484
|
+
|
485
|
+
```bash
|
486
|
+
# List recent sessions (default: 10)
|
487
|
+
claude-swarm list-sessions
|
488
|
+
|
489
|
+
# List more sessions
|
490
|
+
claude-swarm list-sessions --limit 20
|
491
|
+
```
|
492
|
+
|
493
|
+
Output shows:
|
494
|
+
- Session ID (timestamp)
|
495
|
+
- Creation time
|
496
|
+
- Main instance name
|
497
|
+
- Number of instances
|
498
|
+
- Configuration file used
|
499
|
+
- Full session path
|
500
|
+
|
501
|
+
#### Resuming Sessions
|
502
|
+
Resume a previous session with all instances restored to their Claude session states:
|
503
|
+
|
504
|
+
```bash
|
505
|
+
# Resume by session ID
|
506
|
+
claude-swarm --session-id 20241206_143022
|
507
|
+
|
508
|
+
# Resume by full path
|
509
|
+
claude-swarm --session-id ~/.claude-swarm/sessions/my-project/20241206_143022
|
510
|
+
```
|
511
|
+
|
512
|
+
This will:
|
513
|
+
1. Load the session manifest and instance states
|
514
|
+
2. Restore the original swarm configuration
|
515
|
+
3. Resume the main instance with its Claude session ID
|
516
|
+
4. Restore all connected instances with their session IDs
|
517
|
+
5. Maintain the same working directories and tool permissions
|
518
|
+
|
519
|
+
#### How Session Restoration Works
|
520
|
+
- Each instance's Claude session ID is automatically captured and persisted
|
521
|
+
- Instance states are stored in separate files named by instance ID to prevent concurrency issues
|
522
|
+
- MCP configurations are regenerated with the saved session IDs
|
523
|
+
- The main instance uses Claude's `--resume` flag (limited effectiveness)
|
524
|
+
- Connected instances receive their session IDs via `--claude-session-id`
|
525
|
+
|
526
|
+
**Important Limitations:**
|
527
|
+
- The main instance's conversation history and context are not fully restored
|
528
|
+
- Only the session ID is preserved, not the actual conversation state
|
529
|
+
- Connected instances restore more reliably than the main instance
|
530
|
+
- This is an experimental feature and may not work as expected
|
531
|
+
|
460
532
|
## How It Works
|
461
533
|
|
462
534
|
1. **Configuration Parsing**: Claude Swarm reads your YAML configuration and validates it
|
@@ -471,20 +543,17 @@ claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TI
|
|
471
543
|
- Eliminates the need to manually accept each tool or use global `--vibe` mode
|
472
544
|
- Per-instance `vibe: true` skips all permission checks for that specific instance
|
473
545
|
- The permission MCP uses `--permission-prompt-tool` to check tool access
|
474
|
-
- Permission decisions are logged to
|
475
|
-
4. **Session
|
476
|
-
- Generates a shared session
|
477
|
-
- Each instance
|
478
|
-
-
|
546
|
+
- Permission decisions are logged to `~/.claude-swarm/sessions/{project}/{timestamp}/permissions.log`
|
547
|
+
4. **Session Persistence**: Claude Swarm automatically tracks session state:
|
548
|
+
- Generates a shared session path for all instances
|
549
|
+
- Each instance's Claude session ID is captured and saved
|
550
|
+
- Instance states are stored using instance IDs as filenames to avoid conflicts
|
551
|
+
- Sessions can be fully restored with all instances reconnected
|
479
552
|
5. **Main Instance Launch**: The main instance is launched with its MCP configuration, giving it access to all connected instances
|
480
553
|
6. **Inter-Instance Communication**: Connected instances expose themselves as MCP servers with these tools:
|
481
554
|
- **task**: Execute tasks using Claude Code with configurable tools and return results. The tool description includes the instance name and description (e.g., "Execute a task using Agent frontend_dev. Frontend developer specializing in React and TypeScript")
|
482
555
|
- **session_info**: Get current Claude session information including ID and working directory
|
483
556
|
- **reset_session**: Reset the Claude session for a fresh start
|
484
|
-
7. **Session Management**: All session files are organized in `.claude-swarm/sessions/{timestamp}/`:
|
485
|
-
- MCP configuration files: `{instance_name}.mcp.json`
|
486
|
-
- Session log: `session.log` with detailed request/response tracking
|
487
|
-
- Permission log: `permissions.log` with all permission checks and decisions
|
488
557
|
|
489
558
|
## Troubleshooting
|
490
559
|
|
@@ -507,14 +576,15 @@ claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TI
|
|
507
576
|
### Debug Output
|
508
577
|
|
509
578
|
The swarm will display:
|
510
|
-
- Session directory location (
|
579
|
+
- Session directory location (`~/.claude-swarm/sessions/{project}/{timestamp}/`)
|
511
580
|
- Main instance details (model, directory, tools, connections)
|
512
581
|
- The exact command being run
|
513
582
|
|
514
583
|
### Session Files
|
515
584
|
|
516
|
-
Check the session directory
|
517
|
-
- `session.log`:
|
585
|
+
Check the session directory `~/.claude-swarm/sessions/{project}/{timestamp}/` for:
|
586
|
+
- `session.log`: Human-readable logs with request/response tracking
|
587
|
+
- `session.log.json`: All events in JSONL format (one JSON object per line)
|
518
588
|
- `{instance}.mcp.json`: MCP configuration for each instance
|
519
589
|
- All files for a session are kept together for easy review
|
520
590
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
version: 1
|
2
|
+
swarm:
|
3
|
+
name: "Session Restoration Demo"
|
4
|
+
main: coordinator
|
5
|
+
instances:
|
6
|
+
coordinator:
|
7
|
+
description: "Main coordinator that manages the team"
|
8
|
+
directory: .
|
9
|
+
model: sonnet
|
10
|
+
prompt: "You are the team coordinator. You help manage tasks and coordinate between team members."
|
11
|
+
allowed_tools: [Read, Edit, Bash]
|
12
|
+
connections: [developer]
|
13
|
+
|
14
|
+
developer:
|
15
|
+
description: "Developer who writes code"
|
16
|
+
directory: .
|
17
|
+
model: sonnet
|
18
|
+
prompt: "You are a skilled developer who writes clean, well-tested code."
|
19
|
+
allowed_tools: [Read, Edit, Write, Bash]
|
@@ -10,15 +10,19 @@ module ClaudeSwarm
|
|
10
10
|
class ClaudeCodeExecutor
|
11
11
|
attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
|
12
12
|
|
13
|
-
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
|
13
|
+
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
|
14
|
+
instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
|
15
|
+
claude_session_id: nil)
|
14
16
|
@working_directory = working_directory
|
15
17
|
@model = model
|
16
18
|
@mcp_config = mcp_config
|
17
19
|
@vibe = vibe
|
18
|
-
@session_id =
|
20
|
+
@session_id = claude_session_id
|
19
21
|
@last_response = nil
|
20
22
|
@instance_name = instance_name
|
23
|
+
@instance_id = instance_id
|
21
24
|
@calling_instance = calling_instance
|
25
|
+
@calling_instance_id = calling_instance_id
|
22
26
|
|
23
27
|
# Setup logging
|
24
28
|
setup_logging
|
@@ -51,7 +55,10 @@ module ClaudeSwarm
|
|
51
55
|
log_streaming_event(json_data)
|
52
56
|
|
53
57
|
# Capture session_id from system init
|
54
|
-
|
58
|
+
if json_data["type"] == "system" && json_data["subtype"] == "init"
|
59
|
+
@session_id = json_data["session_id"]
|
60
|
+
write_instance_state
|
61
|
+
end
|
55
62
|
|
56
63
|
# Capture the final result
|
57
64
|
result_response = json_data if json_data["type"] == "result"
|
@@ -92,6 +99,27 @@ module ClaudeSwarm
|
|
92
99
|
|
93
100
|
private
|
94
101
|
|
102
|
+
def write_instance_state
|
103
|
+
return unless @instance_id && @session_id
|
104
|
+
|
105
|
+
state_dir = File.join(@session_path, "state")
|
106
|
+
FileUtils.mkdir_p(state_dir)
|
107
|
+
|
108
|
+
state_file = File.join(state_dir, "#{@instance_id}.json")
|
109
|
+
state_data = {
|
110
|
+
instance_name: @instance_name,
|
111
|
+
instance_id: @instance_id,
|
112
|
+
claude_session_id: @session_id,
|
113
|
+
status: "active",
|
114
|
+
updated_at: Time.now.iso8601
|
115
|
+
}
|
116
|
+
|
117
|
+
File.write(state_file, JSON.pretty_generate(state_data))
|
118
|
+
@logger.info("Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}")
|
119
|
+
rescue StandardError => e
|
120
|
+
@logger.error("Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}")
|
121
|
+
end
|
122
|
+
|
95
123
|
def setup_logging
|
96
124
|
# Use session path from environment (required)
|
97
125
|
@session_path = SessionPath.from_env
|
@@ -108,16 +136,41 @@ module ClaudeSwarm
|
|
108
136
|
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
|
109
137
|
end
|
110
138
|
|
111
|
-
|
139
|
+
return unless @instance_name
|
140
|
+
|
141
|
+
instance_info = @instance_name
|
142
|
+
instance_info += " (#{@instance_id})" if @instance_id
|
143
|
+
@logger.info("Started Claude Code executor for instance: #{instance_info}")
|
112
144
|
end
|
113
145
|
|
114
146
|
def log_request(prompt)
|
115
|
-
|
147
|
+
caller_info = @calling_instance
|
148
|
+
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
149
|
+
instance_info = @instance_name
|
150
|
+
instance_info += " (#{@instance_id})" if @instance_id
|
151
|
+
@logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
|
152
|
+
|
153
|
+
# Build event hash for JSON logging
|
154
|
+
event = {
|
155
|
+
type: "request",
|
156
|
+
from_instance: @calling_instance,
|
157
|
+
from_instance_id: @calling_instance_id,
|
158
|
+
to_instance: @instance_name,
|
159
|
+
to_instance_id: @instance_id,
|
160
|
+
prompt: prompt,
|
161
|
+
timestamp: Time.now.iso8601
|
162
|
+
}
|
163
|
+
|
164
|
+
append_to_session_json(event)
|
116
165
|
end
|
117
166
|
|
118
167
|
def log_response(response)
|
168
|
+
caller_info = @calling_instance
|
169
|
+
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
170
|
+
instance_info = @instance_name
|
171
|
+
instance_info += " (#{@instance_id})" if @instance_id
|
119
172
|
@logger.info(
|
120
|
-
"($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{
|
173
|
+
"($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---"
|
121
174
|
)
|
122
175
|
end
|
123
176
|
|
@@ -151,14 +204,18 @@ module ClaudeSwarm
|
|
151
204
|
arguments = tool_call["input"].to_json
|
152
205
|
arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
|
153
206
|
|
207
|
+
instance_info = @instance_name
|
208
|
+
instance_info += " (#{@instance_id})" if @instance_id
|
154
209
|
@logger.info(
|
155
|
-
"Tool call from #{
|
210
|
+
"Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
|
156
211
|
)
|
157
212
|
end
|
158
213
|
|
159
214
|
text = content.select { |c| c["type"] == "text" }
|
160
215
|
text.each do |t|
|
161
|
-
@
|
216
|
+
instance_info = @instance_name
|
217
|
+
instance_info += " (#{@instance_id})" if @instance_id
|
218
|
+
@logger.info("#{instance_info} is thinking:\n---\n#{t["text"]}\n---")
|
162
219
|
end
|
163
220
|
end
|
164
221
|
|
@@ -177,7 +234,9 @@ module ClaudeSwarm
|
|
177
234
|
# Create entry with metadata
|
178
235
|
entry = {
|
179
236
|
instance: @instance_name,
|
237
|
+
instance_id: @instance_id,
|
180
238
|
calling_instance: @calling_instance,
|
239
|
+
calling_instance_id: @calling_instance_id,
|
181
240
|
timestamp: Time.now.iso8601,
|
182
241
|
event: event
|
183
242
|
}
|
@@ -6,24 +6,29 @@ require_relative "claude_code_executor"
|
|
6
6
|
require_relative "task_tool"
|
7
7
|
require_relative "session_info_tool"
|
8
8
|
require_relative "reset_session_tool"
|
9
|
+
require_relative "process_tracker"
|
9
10
|
|
10
11
|
module ClaudeSwarm
|
11
12
|
class ClaudeMcpServer
|
12
13
|
# Class variables to share state with tool classes
|
13
14
|
class << self
|
14
|
-
attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance
|
15
|
+
attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
|
15
16
|
end
|
16
17
|
|
17
|
-
def initialize(instance_config, calling_instance:)
|
18
|
+
def initialize(instance_config, calling_instance:, calling_instance_id: nil)
|
18
19
|
@instance_config = instance_config
|
19
20
|
@calling_instance = calling_instance
|
21
|
+
@calling_instance_id = calling_instance_id
|
20
22
|
@executor = ClaudeCodeExecutor.new(
|
21
23
|
working_directory: instance_config[:directory],
|
22
24
|
model: instance_config[:model],
|
23
25
|
mcp_config: instance_config[:mcp_config_path],
|
24
26
|
vibe: instance_config[:vibe],
|
25
27
|
instance_name: instance_config[:name],
|
26
|
-
|
28
|
+
instance_id: instance_config[:instance_id],
|
29
|
+
calling_instance: calling_instance,
|
30
|
+
calling_instance_id: calling_instance_id,
|
31
|
+
claude_session_id: instance_config[:claude_session_id]
|
27
32
|
)
|
28
33
|
|
29
34
|
# Set class variables so tools can access them
|
@@ -32,9 +37,16 @@ module ClaudeSwarm
|
|
32
37
|
self.class.logger = @executor.logger
|
33
38
|
self.class.session_path = @executor.session_path
|
34
39
|
self.class.calling_instance = @calling_instance
|
40
|
+
self.class.calling_instance_id = @calling_instance_id
|
35
41
|
end
|
36
42
|
|
37
43
|
def start
|
44
|
+
# Track this process
|
45
|
+
if @executor.session_path && File.exist?(@executor.session_path)
|
46
|
+
tracker = ProcessTracker.new(@executor.session_path)
|
47
|
+
tracker.track_pid(Process.pid, "mcp_#{@instance_config[:name]}")
|
48
|
+
end
|
49
|
+
|
38
50
|
server = FastMcp::Server.new(
|
39
51
|
name: @instance_config[:name],
|
40
52
|
version: "1.0.0"
|
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}"
|
@@ -81,6 +90,12 @@ module ClaudeSwarm
|
|
81
90
|
desc: "Run with --dangerously-skip-permissions"
|
82
91
|
method_option :calling_instance, type: :string, required: true,
|
83
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"
|
84
99
|
def mcp_serve
|
85
100
|
instance_config = {
|
86
101
|
name: options[:name],
|
@@ -91,11 +106,17 @@ module ClaudeSwarm
|
|
91
106
|
tools: options[:tools] || [],
|
92
107
|
disallowed_tools: options[:disallowed_tools] || [],
|
93
108
|
mcp_config_path: options[:mcp_config_path],
|
94
|
-
vibe: options[:vibe]
|
109
|
+
vibe: options[:vibe],
|
110
|
+
instance_id: options[:instance_id],
|
111
|
+
claude_session_id: options[:claude_session_id]
|
95
112
|
}
|
96
113
|
|
97
114
|
begin
|
98
|
-
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
|
+
)
|
99
120
|
server.start
|
100
121
|
rescue StandardError => e
|
101
122
|
error "Error starting MCP server: #{e.message}"
|
@@ -171,6 +192,79 @@ module ClaudeSwarm
|
|
171
192
|
say "Claude Swarm #{VERSION}"
|
172
193
|
end
|
173
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
|
+
|
174
268
|
desc "tools-mcp", "Start a permission management MCP server for tool access control"
|
175
269
|
method_option :allowed_tools, aliases: "-t", type: :string,
|
176
270
|
desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
|
@@ -194,5 +288,77 @@ module ClaudeSwarm
|
|
194
288
|
def error(message)
|
195
289
|
say message, :red
|
196
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
|
197
363
|
end
|
198
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,11 +60,11 @@ 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
|
@@ -3,19 +3,33 @@
|
|
3
3
|
require "json"
|
4
4
|
require "fileutils"
|
5
5
|
require "shellwords"
|
6
|
+
require "securerandom"
|
6
7
|
require_relative "session_path"
|
7
8
|
|
8
9
|
module ClaudeSwarm
|
9
10
|
class McpGenerator
|
10
|
-
def initialize(configuration, vibe: false)
|
11
|
+
def initialize(configuration, vibe: false, restore_session_path: nil)
|
11
12
|
@config = configuration
|
12
13
|
@vibe = vibe
|
14
|
+
@restore_session_path = restore_session_path
|
13
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
|
14
18
|
end
|
15
19
|
|
16
20
|
def generate_all
|
17
21
|
ensure_swarm_directory
|
18
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
|
+
|
19
33
|
@config.instances.each do |name, instance|
|
20
34
|
generate_mcp_config(name, instance)
|
21
35
|
end
|
@@ -48,13 +62,18 @@ module ClaudeSwarm
|
|
48
62
|
# Add connection MCPs for other instances
|
49
63
|
instance[:connections].each do |connection_name|
|
50
64
|
connected_instance = @config.instances[connection_name]
|
51
|
-
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
|
+
)
|
52
69
|
end
|
53
70
|
|
54
71
|
# Add permission MCP server if not in vibe mode (global or instance-specific)
|
55
72
|
mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools], instance[:disallowed_tools]) unless @vibe || instance[:vibe]
|
56
73
|
|
57
74
|
config = {
|
75
|
+
"instance_id" => @instance_ids[name],
|
76
|
+
"instance_name" => name,
|
58
77
|
"mcpServers" => mcp_servers
|
59
78
|
}
|
60
79
|
|
@@ -79,7 +98,7 @@ module ClaudeSwarm
|
|
79
98
|
end
|
80
99
|
end
|
81
100
|
|
82
|
-
def build_instance_mcp_config(name, instance, calling_instance:)
|
101
|
+
def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
|
83
102
|
# Get the path to the claude-swarm executable
|
84
103
|
exe_path = "claude-swarm"
|
85
104
|
|
@@ -104,8 +123,18 @@ module ClaudeSwarm
|
|
104
123
|
|
105
124
|
args.push("--calling-instance", calling_instance) if calling_instance
|
106
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
|
+
|
107
130
|
args.push("--vibe") if @vibe || instance[:vibe]
|
108
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
|
+
|
109
138
|
{
|
110
139
|
"type" => "stdio",
|
111
140
|
"command" => exe_path,
|
@@ -113,6 +142,27 @@ module ClaudeSwarm
|
|
113
142
|
}
|
114
143
|
end
|
115
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
|
+
|
116
166
|
def build_permission_mcp_config(allowed_tools, disallowed_tools)
|
117
167
|
exe_path = "claude-swarm"
|
118
168
|
|
@@ -1,43 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "shellwords"
|
4
|
+
require "json"
|
5
|
+
require "fileutils"
|
4
6
|
require_relative "session_path"
|
7
|
+
require_relative "process_tracker"
|
5
8
|
|
6
9
|
module ClaudeSwarm
|
7
10
|
class Orchestrator
|
8
|
-
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false
|
11
|
+
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
|
12
|
+
restore_session_path: nil)
|
9
13
|
@config = configuration
|
10
14
|
@generator = mcp_generator
|
11
15
|
@vibe = vibe
|
12
16
|
@prompt = prompt
|
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
|
-
|
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
|
27
34
|
|
28
|
-
|
29
|
-
|
35
|
+
unless @prompt
|
36
|
+
puts "📝 Using existing session: #{session_path}/"
|
37
|
+
puts
|
38
|
+
end
|
30
39
|
|
31
|
-
|
32
|
-
|
33
|
-
puts
|
34
|
-
end
|
40
|
+
# Initialize process tracker
|
41
|
+
@process_tracker = ProcessTracker.new(session_path)
|
35
42
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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)
|
41
86
|
end
|
42
87
|
|
43
88
|
# Launch the main instance
|
@@ -69,14 +114,44 @@ module ClaudeSwarm
|
|
69
114
|
end
|
70
115
|
|
71
116
|
# Clean up log streaming thread
|
72
|
-
|
117
|
+
if log_thread
|
118
|
+
log_thread.terminate
|
119
|
+
log_thread.join
|
120
|
+
end
|
73
121
|
|
74
|
-
|
75
|
-
|
122
|
+
# Clean up child processes
|
123
|
+
cleanup_processes
|
76
124
|
end
|
77
125
|
|
78
126
|
private
|
79
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
|
+
|
80
155
|
def start_log_streaming
|
81
156
|
Thread.new do
|
82
157
|
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
@@ -110,6 +185,26 @@ module ClaudeSwarm
|
|
110
185
|
instance[:model]
|
111
186
|
]
|
112
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
|
+
|
113
208
|
if @vibe || instance[:vibe]
|
114
209
|
parts << "--dangerously-skip-permissions"
|
115
210
|
else
|
@@ -6,6 +6,7 @@ require "logger"
|
|
6
6
|
require "fileutils"
|
7
7
|
require_relative "permission_tool"
|
8
8
|
require_relative "session_path"
|
9
|
+
require_relative "process_tracker"
|
9
10
|
|
10
11
|
module ClaudeSwarm
|
11
12
|
class PermissionMcpServer
|
@@ -46,6 +47,13 @@ module ClaudeSwarm
|
|
46
47
|
end
|
47
48
|
|
48
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
|
+
|
49
57
|
server = FastMcp::Server.new(
|
50
58
|
name: SERVER_NAME,
|
51
59
|
version: SERVER_VERSION
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module ClaudeSwarm
|
6
|
+
class ProcessTracker
|
7
|
+
PIDS_DIR = "pids"
|
8
|
+
|
9
|
+
def initialize(session_path)
|
10
|
+
@session_path = session_path
|
11
|
+
@pids_dir = File.join(@session_path, PIDS_DIR)
|
12
|
+
ensure_pids_directory
|
13
|
+
end
|
14
|
+
|
15
|
+
def track_pid(pid, name)
|
16
|
+
pid_file = File.join(@pids_dir, pid.to_s)
|
17
|
+
File.write(pid_file, name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cleanup_all
|
21
|
+
return unless Dir.exist?(@pids_dir)
|
22
|
+
|
23
|
+
# Get all PID files
|
24
|
+
pid_files = Dir.glob(File.join(@pids_dir, "*"))
|
25
|
+
|
26
|
+
pid_files.each do |pid_file|
|
27
|
+
pid = File.basename(pid_file).to_i
|
28
|
+
name = begin
|
29
|
+
File.read(pid_file).strip
|
30
|
+
rescue StandardError
|
31
|
+
"unknown"
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
# Check if process is still running
|
36
|
+
Process.kill(0, pid)
|
37
|
+
# If we get here, process is running, so kill it
|
38
|
+
Process.kill("TERM", pid)
|
39
|
+
puts "✓ Terminated MCP server: #{name} (PID: #{pid})"
|
40
|
+
|
41
|
+
# Give it a moment to terminate gracefully
|
42
|
+
sleep 0.1
|
43
|
+
|
44
|
+
# Force kill if still running
|
45
|
+
begin
|
46
|
+
Process.kill(0, pid)
|
47
|
+
Process.kill("KILL", pid)
|
48
|
+
puts " → Force killed #{name} (PID: #{pid})"
|
49
|
+
rescue Errno::ESRCH
|
50
|
+
# Process is gone, which is what we want
|
51
|
+
end
|
52
|
+
rescue Errno::ESRCH
|
53
|
+
# Process not found, already terminated
|
54
|
+
puts " → MCP server #{name} (PID: #{pid}) already terminated"
|
55
|
+
rescue Errno::EPERM
|
56
|
+
# Permission denied
|
57
|
+
puts " ⚠️ No permission to terminate #{name} (PID: #{pid})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Clean up the pids directory
|
62
|
+
FileUtils.rm_rf(@pids_dir)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.cleanup_session(session_path)
|
66
|
+
return unless Dir.exist?(File.join(session_path, PIDS_DIR))
|
67
|
+
|
68
|
+
tracker = new(session_path)
|
69
|
+
tracker.cleanup_all
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def ensure_pids_directory
|
75
|
+
FileUtils.mkdir_p(@pids_dir)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/claude_swarm/version.rb
CHANGED
data/lib/claude_swarm.rb
CHANGED
@@ -2,10 +2,18 @@
|
|
2
2
|
|
3
3
|
require_relative "claude_swarm/version"
|
4
4
|
require_relative "claude_swarm/cli"
|
5
|
+
require_relative "claude_swarm/configuration"
|
6
|
+
require_relative "claude_swarm/mcp_generator"
|
7
|
+
require_relative "claude_swarm/orchestrator"
|
5
8
|
require_relative "claude_swarm/claude_code_executor"
|
6
9
|
require_relative "claude_swarm/claude_mcp_server"
|
7
10
|
require_relative "claude_swarm/permission_tool"
|
8
11
|
require_relative "claude_swarm/permission_mcp_server"
|
12
|
+
require_relative "claude_swarm/session_path"
|
13
|
+
require_relative "claude_swarm/session_info_tool"
|
14
|
+
require_relative "claude_swarm/reset_session_tool"
|
15
|
+
require_relative "claude_swarm/task_tool"
|
16
|
+
require_relative "claude_swarm/process_tracker"
|
9
17
|
|
10
18
|
module ClaudeSwarm
|
11
19
|
class Error < StandardError; end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: claude_swarm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paulo Arruda
|
@@ -61,6 +61,7 @@ files:
|
|
61
61
|
- claude-swarm.yml
|
62
62
|
- example/claude-swarm.yml
|
63
63
|
- example/microservices-team.yml
|
64
|
+
- example/session-restoration-demo.yml
|
64
65
|
- example/test-generation.yml
|
65
66
|
- exe/claude-swarm
|
66
67
|
- lib/claude_swarm.rb
|
@@ -72,6 +73,7 @@ files:
|
|
72
73
|
- lib/claude_swarm/orchestrator.rb
|
73
74
|
- lib/claude_swarm/permission_mcp_server.rb
|
74
75
|
- lib/claude_swarm/permission_tool.rb
|
76
|
+
- lib/claude_swarm/process_tracker.rb
|
75
77
|
- lib/claude_swarm/reset_session_tool.rb
|
76
78
|
- lib/claude_swarm/session_info_tool.rb
|
77
79
|
- lib/claude_swarm/session_path.rb
|