claude_swarm 0.1.20 → 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 -66
- data/.rubocop_todo.yml +11 -0
- data/CHANGELOG.md +93 -0
- data/CLAUDE.md +61 -0
- data/README.md +172 -15
- data/Rakefile +1 -1
- data/examples/mixed-provider-swarm.yml +23 -0
- data/lib/claude_swarm/claude_code_executor.rb +7 -12
- data/lib/claude_swarm/claude_mcp_server.rb +26 -12
- data/lib/claude_swarm/cli.rb +293 -165
- 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 -14
- 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 +205 -39
- 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 +1 -3
- 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 +13 -20
- data/lib/claude_swarm.rb +23 -10
- data/single.yml +482 -6
- metadata +50 -16
- 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
data/lib/claude_swarm/cli.rb
CHANGED
@@ -1,107 +1,190 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "thor"
|
4
|
-
require "json"
|
5
|
-
require "erb"
|
6
|
-
|
7
3
|
module ClaudeSwarm
|
8
4
|
class CLI < Thor
|
9
5
|
include SystemUtils
|
10
|
-
|
11
|
-
|
6
|
+
class << self
|
7
|
+
def exit_on_failure?
|
8
|
+
true
|
9
|
+
end
|
12
10
|
end
|
13
11
|
|
14
12
|
desc "start [CONFIG_FILE]", "Start a Claude Swarm from configuration file"
|
15
|
-
method_option :
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
method_option :prompt,
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
method_option :
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
method_option :
|
28
|
-
|
29
|
-
|
13
|
+
method_option :vibe,
|
14
|
+
type: :boolean,
|
15
|
+
default: false,
|
16
|
+
desc: "Run with --dangerously-skip-permissions for all instances"
|
17
|
+
method_option :prompt,
|
18
|
+
aliases: "-p",
|
19
|
+
type: :string,
|
20
|
+
desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
|
21
|
+
method_option :stream_logs,
|
22
|
+
type: :boolean,
|
23
|
+
default: false,
|
24
|
+
desc: "Stream session logs to stdout (only works with -p)"
|
25
|
+
method_option :debug,
|
26
|
+
type: :boolean,
|
27
|
+
default: false,
|
28
|
+
desc: "Enable debug output"
|
29
|
+
method_option :worktree,
|
30
|
+
type: :string,
|
31
|
+
aliases: "-w",
|
32
|
+
desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
|
33
|
+
banner: "[NAME]"
|
34
|
+
method_option :session_id,
|
35
|
+
type: :string,
|
36
|
+
desc: "Use a specific session ID instead of generating one"
|
30
37
|
def start(config_file = nil)
|
31
|
-
|
32
|
-
if options[:session_id]
|
33
|
-
restore_session(options[:session_id])
|
34
|
-
return
|
35
|
-
end
|
36
|
-
|
37
|
-
config_path = config_file || options[:config]
|
38
|
+
config_path = config_file || "claude-swarm.yml"
|
38
39
|
unless File.exist?(config_path)
|
39
|
-
error
|
40
|
-
exit
|
40
|
+
error("Configuration file not found: #{config_path}")
|
41
|
+
exit(1)
|
41
42
|
end
|
42
43
|
|
43
|
-
say
|
44
|
+
say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
|
44
45
|
|
45
46
|
# Validate stream_logs option
|
46
47
|
if options[:stream_logs] && !options[:prompt]
|
47
|
-
error
|
48
|
-
exit
|
48
|
+
error("--stream-logs can only be used with -p/--prompt")
|
49
|
+
exit(1)
|
49
50
|
end
|
50
51
|
|
51
52
|
begin
|
52
|
-
config = Configuration.new(config_path, base_dir: Dir.pwd)
|
53
|
+
config = Configuration.new(config_path, base_dir: Dir.pwd, options: options)
|
53
54
|
generator = McpGenerator.new(config, vibe: options[:vibe])
|
54
|
-
orchestrator = Orchestrator.new(
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
orchestrator = Orchestrator.new(
|
56
|
+
config,
|
57
|
+
generator,
|
58
|
+
vibe: options[:vibe],
|
59
|
+
prompt: options[:prompt],
|
60
|
+
stream_logs: options[:stream_logs],
|
61
|
+
debug: options[:debug],
|
62
|
+
worktree: options[:worktree],
|
63
|
+
session_id: options[:session_id],
|
64
|
+
)
|
60
65
|
orchestrator.start
|
61
66
|
rescue Error => e
|
62
|
-
error
|
63
|
-
exit
|
67
|
+
error(e.message)
|
68
|
+
exit(1)
|
64
69
|
rescue StandardError => e
|
65
|
-
error
|
66
|
-
error
|
67
|
-
exit
|
70
|
+
error("Unexpected error: #{e.message}")
|
71
|
+
error(e.backtrace.join("\n")) if options[:verbose]
|
72
|
+
exit(1)
|
68
73
|
end
|
69
74
|
end
|
70
75
|
|
71
76
|
desc "mcp-serve", "Start an MCP server for a Claude instance"
|
72
|
-
method_option :name,
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
method_option :
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
method_option :
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
method_option :
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
77
|
+
method_option :name,
|
78
|
+
aliases: "-n",
|
79
|
+
type: :string,
|
80
|
+
required: true,
|
81
|
+
desc: "Instance name"
|
82
|
+
method_option :directory,
|
83
|
+
aliases: "-d",
|
84
|
+
type: :string,
|
85
|
+
required: true,
|
86
|
+
desc: "Working directory for the instance"
|
87
|
+
method_option :directories,
|
88
|
+
type: :array,
|
89
|
+
desc: "All directories (including main directory) for the instance"
|
90
|
+
method_option :model,
|
91
|
+
aliases: "-m",
|
92
|
+
type: :string,
|
93
|
+
required: true,
|
94
|
+
desc: "Claude model to use (e.g., opus, sonnet)"
|
95
|
+
method_option :prompt,
|
96
|
+
aliases: "-p",
|
97
|
+
type: :string,
|
98
|
+
desc: "System prompt for the instance"
|
99
|
+
method_option :description,
|
100
|
+
type: :string,
|
101
|
+
desc: "Description of the instance's role"
|
102
|
+
method_option :allowed_tools,
|
103
|
+
aliases: "-t",
|
104
|
+
type: :array,
|
105
|
+
desc: "Allowed tools for the instance"
|
106
|
+
method_option :disallowed_tools,
|
107
|
+
type: :array,
|
108
|
+
desc: "Disallowed tools for the instance"
|
109
|
+
method_option :connections,
|
110
|
+
type: :array,
|
111
|
+
desc: "Connections to other instances"
|
112
|
+
method_option :mcp_config_path,
|
113
|
+
type: :string,
|
114
|
+
desc: "Path to MCP configuration file"
|
115
|
+
method_option :debug,
|
116
|
+
type: :boolean,
|
117
|
+
default: false,
|
118
|
+
desc: "Enable debug output"
|
119
|
+
method_option :vibe,
|
120
|
+
type: :boolean,
|
121
|
+
default: false,
|
122
|
+
desc: "Run with --dangerously-skip-permissions"
|
123
|
+
method_option :calling_instance,
|
124
|
+
type: :string,
|
125
|
+
required: true,
|
126
|
+
desc: "Name of the instance that launched this MCP server"
|
127
|
+
method_option :calling_instance_id,
|
128
|
+
type: :string,
|
129
|
+
desc: "Unique ID of the instance that launched this MCP server"
|
130
|
+
method_option :instance_id,
|
131
|
+
type: :string,
|
132
|
+
desc: "Unique ID of this instance"
|
133
|
+
method_option :claude_session_id,
|
134
|
+
type: :string,
|
135
|
+
desc: "Claude session ID to resume"
|
136
|
+
method_option :provider,
|
137
|
+
type: :string,
|
138
|
+
desc: "Provider to use (claude or openai)"
|
139
|
+
method_option :temperature,
|
140
|
+
type: :numeric,
|
141
|
+
desc: "Temperature for OpenAI models"
|
142
|
+
method_option :api_version,
|
143
|
+
type: :string,
|
144
|
+
desc: "API version for OpenAI (chat_completion or responses)"
|
145
|
+
method_option :openai_token_env,
|
146
|
+
type: :string,
|
147
|
+
desc: "Environment variable name for OpenAI API key"
|
148
|
+
method_option :base_url,
|
149
|
+
type: :string,
|
150
|
+
desc: "Base URL for OpenAI API"
|
151
|
+
method_option :reasoning_effort,
|
152
|
+
type: :string,
|
153
|
+
desc: "Reasoning effort for OpenAI models"
|
104
154
|
def mcp_serve
|
155
|
+
# Validate reasoning_effort if provided
|
156
|
+
if options[:reasoning_effort]
|
157
|
+
# Only validate if provider is openai (or not specified, since it could be set elsewhere)
|
158
|
+
if options[:provider] && options[:provider] != "openai"
|
159
|
+
error("reasoning_effort is only supported for OpenAI models")
|
160
|
+
exit(1)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate it's used with an o-series model
|
164
|
+
model = options[:model]
|
165
|
+
unless model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
166
|
+
error("reasoning_effort is only supported for o-series models (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)")
|
167
|
+
error("Current model: #{model}")
|
168
|
+
exit(1)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Validate the value
|
172
|
+
unless ClaudeSwarm::Configuration::VALID_REASONING_EFFORTS.include?(options[:reasoning_effort])
|
173
|
+
error("reasoning_effort must be 'low', 'medium', or 'high'")
|
174
|
+
exit(1)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Validate temperature is not used with o-series models
|
179
|
+
if options[:temperature] && options[:provider] == "openai"
|
180
|
+
model = options[:model]
|
181
|
+
if model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
182
|
+
error("temperature parameter is not supported for o-series models (#{model})")
|
183
|
+
error("O-series models use deterministic reasoning and don't accept temperature settings")
|
184
|
+
exit(1)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
105
188
|
instance_config = {
|
106
189
|
name: options[:name],
|
107
190
|
directory: options[:directory],
|
@@ -115,33 +198,42 @@ module ClaudeSwarm
|
|
115
198
|
mcp_config_path: options[:mcp_config_path],
|
116
199
|
vibe: options[:vibe] || false,
|
117
200
|
instance_id: options[:instance_id],
|
118
|
-
claude_session_id: options[:claude_session_id]
|
201
|
+
claude_session_id: options[:claude_session_id],
|
202
|
+
provider: options[:provider],
|
203
|
+
temperature: options[:temperature],
|
204
|
+
api_version: options[:api_version],
|
205
|
+
openai_token_env: options[:openai_token_env],
|
206
|
+
base_url: options[:base_url],
|
207
|
+
reasoning_effort: options[:reasoning_effort],
|
119
208
|
}
|
120
209
|
|
121
210
|
begin
|
122
211
|
server = ClaudeMcpServer.new(
|
123
212
|
instance_config,
|
124
213
|
calling_instance: options[:calling_instance],
|
125
|
-
calling_instance_id: options[:calling_instance_id]
|
214
|
+
calling_instance_id: options[:calling_instance_id],
|
126
215
|
)
|
127
216
|
server.start
|
128
217
|
rescue StandardError => e
|
129
|
-
error
|
130
|
-
error
|
131
|
-
exit
|
218
|
+
error("Error starting MCP server: #{e.message}")
|
219
|
+
error(e.backtrace.join("\n")) if options[:debug]
|
220
|
+
exit(1)
|
132
221
|
end
|
133
222
|
end
|
134
223
|
|
135
224
|
desc "init", "Initialize a new claude-swarm.yml configuration file"
|
136
|
-
method_option :force,
|
137
|
-
|
225
|
+
method_option :force,
|
226
|
+
aliases: "-f",
|
227
|
+
type: :boolean,
|
228
|
+
default: false,
|
229
|
+
desc: "Overwrite existing configuration file"
|
138
230
|
def init
|
139
231
|
config_path = "claude-swarm.yml"
|
140
232
|
|
141
233
|
if File.exist?(config_path) && !options[:force]
|
142
|
-
error
|
143
|
-
error
|
144
|
-
exit
|
234
|
+
error("Configuration file already exists: #{config_path}")
|
235
|
+
error("Use --force to overwrite")
|
236
|
+
exit(1)
|
145
237
|
end
|
146
238
|
|
147
239
|
template = <<~YAML
|
@@ -158,7 +250,8 @@ module ClaudeSwarm
|
|
158
250
|
description: "Lead developer who coordinates the team and makes architectural decisions"
|
159
251
|
directory: .
|
160
252
|
model: sonnet
|
161
|
-
prompt:
|
253
|
+
prompt: |
|
254
|
+
You are the lead developer coordinating the team
|
162
255
|
allowed_tools: [Read, Edit, Bash, Write]
|
163
256
|
# connections: [frontend_dev, backend_dev]
|
164
257
|
|
@@ -168,49 +261,59 @@ module ClaudeSwarm
|
|
168
261
|
# description: "Frontend developer specializing in React and modern web technologies"
|
169
262
|
# directory: ./frontend
|
170
263
|
# model: sonnet
|
171
|
-
# prompt:
|
264
|
+
# prompt: |
|
265
|
+
# You specialize in frontend development with React, TypeScript, and modern web technologies
|
172
266
|
# allowed_tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
|
173
267
|
|
174
268
|
# backend_dev:
|
175
|
-
# description:
|
269
|
+
# description: |
|
270
|
+
# Backend developer focusing on APIs, databases, and server architecture
|
176
271
|
# directory: ../other-app/backend
|
177
272
|
# model: sonnet
|
178
|
-
# prompt:
|
273
|
+
# prompt: |
|
274
|
+
# You specialize in backend development, APIs, databases, and server architecture
|
179
275
|
# allowed_tools: [Read, Edit, Write, Bash]
|
180
276
|
|
181
277
|
# devops_engineer:
|
182
278
|
# description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
|
183
279
|
# directory: .
|
184
280
|
# model: sonnet
|
185
|
-
# prompt:
|
281
|
+
# prompt: |
|
282
|
+
# You specialize in infrastrujcture, CI/CD, containerization, and deployment
|
186
283
|
# allowed_tools: [Read, Edit, Write, "Bash(docker:*)", "Bash(kubectl:*)", "Bash(terraform:*)"]
|
187
284
|
|
188
285
|
# qa_engineer:
|
189
286
|
# description: "QA engineer ensuring quality through comprehensive testing"
|
190
287
|
# directory: ./tests
|
191
288
|
# model: sonnet
|
192
|
-
# prompt:
|
289
|
+
# prompt: |
|
290
|
+
# You specialize in testing, quality assurance, and test automation
|
193
291
|
# allowed_tools: [Read, Edit, Write, Bash]
|
194
292
|
YAML
|
195
293
|
|
196
294
|
File.write(config_path, template)
|
197
|
-
say
|
198
|
-
say
|
295
|
+
say("Created #{config_path}", :green)
|
296
|
+
say("Edit this file to configure your swarm, then run 'claude-swarm' to start")
|
199
297
|
end
|
200
298
|
|
201
299
|
desc "generate", "Launch Claude to help generate a swarm configuration interactively"
|
202
|
-
method_option :output,
|
203
|
-
|
204
|
-
|
205
|
-
|
300
|
+
method_option :output,
|
301
|
+
aliases: "-o",
|
302
|
+
type: :string,
|
303
|
+
desc: "Output file path for the generated configuration"
|
304
|
+
method_option :model,
|
305
|
+
aliases: "-m",
|
306
|
+
type: :string,
|
307
|
+
default: "sonnet",
|
308
|
+
desc: "Claude model to use for generation"
|
206
309
|
def generate
|
207
310
|
# Check if claude command exists
|
208
311
|
begin
|
209
312
|
system!("command -v claude > /dev/null 2>&1")
|
210
313
|
rescue Error
|
211
|
-
error
|
212
|
-
say
|
213
|
-
exit
|
314
|
+
error("Claude CLI is not installed or not in PATH")
|
315
|
+
say("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
|
316
|
+
exit(1)
|
214
317
|
end
|
215
318
|
|
216
319
|
# Read README for context about claude-swarm capabilities
|
@@ -223,8 +326,9 @@ module ClaudeSwarm
|
|
223
326
|
# Launch Claude in interactive mode with the initial prompt
|
224
327
|
cmd = [
|
225
328
|
"claude",
|
226
|
-
"--model",
|
227
|
-
|
329
|
+
"--model",
|
330
|
+
options[:model],
|
331
|
+
preprompt,
|
228
332
|
]
|
229
333
|
|
230
334
|
# Execute and let the user take over
|
@@ -233,7 +337,7 @@ module ClaudeSwarm
|
|
233
337
|
|
234
338
|
desc "version", "Show Claude Swarm version"
|
235
339
|
def version
|
236
|
-
say
|
340
|
+
say("Claude Swarm #{VERSION}")
|
237
341
|
end
|
238
342
|
|
239
343
|
desc "ps", "List running Claude Swarm sessions"
|
@@ -247,8 +351,11 @@ module ClaudeSwarm
|
|
247
351
|
end
|
248
352
|
|
249
353
|
desc "clean", "Remove stale session symlinks and orphaned worktrees"
|
250
|
-
method_option :days,
|
251
|
-
|
354
|
+
method_option :days,
|
355
|
+
aliases: "-d",
|
356
|
+
type: :numeric,
|
357
|
+
default: 7,
|
358
|
+
desc: "Remove sessions older than N days"
|
252
359
|
def clean
|
253
360
|
# Clean stale symlinks
|
254
361
|
cleaned_symlinks = clean_stale_symlinks(options[:days])
|
@@ -257,49 +364,60 @@ module ClaudeSwarm
|
|
257
364
|
cleaned_worktrees = clean_orphaned_worktrees(options[:days])
|
258
365
|
|
259
366
|
if cleaned_symlinks.positive? || cleaned_worktrees.positive?
|
260
|
-
say
|
261
|
-
say
|
367
|
+
say("Cleaned #{cleaned_symlinks} stale symlink#{"s" unless cleaned_symlinks == 1}", :green)
|
368
|
+
say("Cleaned #{cleaned_worktrees} orphaned worktree#{"s" unless cleaned_worktrees == 1}", :green)
|
262
369
|
else
|
263
|
-
say
|
370
|
+
say("No cleanup needed", :green)
|
264
371
|
end
|
265
372
|
end
|
266
373
|
|
374
|
+
desc "restore SESSION_ID", "Restore a previous session by ID"
|
375
|
+
def restore(session_id)
|
376
|
+
restore_session(session_id)
|
377
|
+
end
|
378
|
+
|
267
379
|
desc "watch SESSION_ID", "Watch session logs"
|
268
|
-
method_option :lines,
|
269
|
-
|
380
|
+
method_option :lines,
|
381
|
+
aliases: "-n",
|
382
|
+
type: :numeric,
|
383
|
+
default: 100,
|
384
|
+
desc: "Number of lines to show initially"
|
270
385
|
def watch(session_id)
|
271
386
|
# Find session path
|
272
387
|
run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
|
273
388
|
session_path = if File.symlink?(run_symlink)
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
389
|
+
File.readlink(run_symlink)
|
390
|
+
else
|
391
|
+
# Search in sessions directory
|
392
|
+
Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
|
393
|
+
File.basename(path) == session_id
|
394
|
+
end
|
395
|
+
end
|
281
396
|
|
282
397
|
unless session_path && Dir.exist?(session_path)
|
283
|
-
error
|
284
|
-
exit
|
398
|
+
error("Session not found: #{session_id}")
|
399
|
+
exit(1)
|
285
400
|
end
|
286
401
|
|
287
402
|
log_file = File.join(session_path, "session.log")
|
288
403
|
unless File.exist?(log_file)
|
289
|
-
error
|
290
|
-
exit
|
404
|
+
error("Log file not found for session: #{session_id}")
|
405
|
+
exit(1)
|
291
406
|
end
|
292
407
|
|
293
408
|
exec("tail", "-f", "-n", options[:lines].to_s, log_file)
|
294
409
|
end
|
295
410
|
|
296
411
|
desc "list-sessions", "List all available Claude Swarm sessions"
|
297
|
-
method_option :limit,
|
298
|
-
|
412
|
+
method_option :limit,
|
413
|
+
aliases: "-l",
|
414
|
+
type: :numeric,
|
415
|
+
default: 10,
|
416
|
+
desc: "Maximum number of sessions to display"
|
299
417
|
def list_sessions
|
300
418
|
sessions_dir = File.expand_path("~/.claude-swarm/sessions")
|
301
419
|
unless Dir.exist?(sessions_dir)
|
302
|
-
say
|
420
|
+
say("No sessions found", :yellow)
|
303
421
|
return
|
304
422
|
end
|
305
423
|
|
@@ -335,7 +453,7 @@ module ClaudeSwarm
|
|
335
453
|
main_instance: main_instance,
|
336
454
|
instances_count: mcp_files.size,
|
337
455
|
swarm_name: swarm_name,
|
338
|
-
config_path: config_file
|
456
|
+
config_path: config_file,
|
339
457
|
}
|
340
458
|
rescue StandardError
|
341
459
|
# Skip invalid manifests
|
@@ -343,7 +461,7 @@ module ClaudeSwarm
|
|
343
461
|
end
|
344
462
|
|
345
463
|
if sessions.empty?
|
346
|
-
say
|
464
|
+
say("No sessions found", :yellow)
|
347
465
|
return
|
348
466
|
end
|
349
467
|
|
@@ -352,18 +470,18 @@ module ClaudeSwarm
|
|
352
470
|
sessions = sessions.first(options[:limit])
|
353
471
|
|
354
472
|
# Display sessions
|
355
|
-
say
|
473
|
+
say("\nAvailable sessions (newest first):\n", :bold)
|
356
474
|
sessions.each do |session|
|
357
|
-
say
|
358
|
-
say
|
359
|
-
say
|
360
|
-
say
|
361
|
-
say
|
362
|
-
say
|
475
|
+
say("\n#{session[:project]}/#{session[:id]}", :green)
|
476
|
+
say(" Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}")
|
477
|
+
say(" Main: #{session[:main_instance]}")
|
478
|
+
say(" Instances: #{session[:instances_count]}")
|
479
|
+
say(" Swarm: #{session[:swarm_name]}")
|
480
|
+
say(" Config: #{session[:config_path]}", :cyan)
|
363
481
|
end
|
364
482
|
|
365
|
-
say
|
366
|
-
say
|
483
|
+
say("\nTo resume a session, run:", :bold)
|
484
|
+
say(" claude-swarm restore <session-id>", :cyan)
|
367
485
|
end
|
368
486
|
|
369
487
|
default_task :start
|
@@ -371,33 +489,33 @@ module ClaudeSwarm
|
|
371
489
|
private
|
372
490
|
|
373
491
|
def error(message)
|
374
|
-
say
|
492
|
+
say(message, :red)
|
375
493
|
end
|
376
494
|
|
377
495
|
def restore_session(session_id)
|
378
|
-
say
|
496
|
+
say("Restoring session: #{session_id}", :green)
|
379
497
|
|
380
498
|
# Find the session path
|
381
499
|
session_path = find_session_path(session_id)
|
382
500
|
unless session_path
|
383
|
-
error
|
384
|
-
exit
|
501
|
+
error("Session not found: #{session_id}")
|
502
|
+
exit(1)
|
385
503
|
end
|
386
504
|
|
387
505
|
begin
|
388
506
|
# Load session info from instance ID in MCP config
|
389
507
|
mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
|
390
508
|
if mcp_files.empty?
|
391
|
-
error
|
392
|
-
exit
|
509
|
+
error("No MCP configuration files found in session")
|
510
|
+
exit(1)
|
393
511
|
end
|
394
512
|
|
395
513
|
# Load the configuration from the session directory
|
396
514
|
config_file = File.join(session_path, "config.yml")
|
397
515
|
|
398
516
|
unless File.exist?(config_file)
|
399
|
-
error
|
400
|
-
exit
|
517
|
+
error("Configuration file not found in session")
|
518
|
+
exit(1)
|
401
519
|
end
|
402
520
|
|
403
521
|
# Change to the original start directory if it exists
|
@@ -406,10 +524,10 @@ module ClaudeSwarm
|
|
406
524
|
original_dir = File.read(start_dir_file).strip
|
407
525
|
if Dir.exist?(original_dir)
|
408
526
|
Dir.chdir(original_dir)
|
409
|
-
say
|
527
|
+
say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
|
410
528
|
else
|
411
|
-
error
|
412
|
-
exit
|
529
|
+
error("Original directory no longer exists: #{original_dir}")
|
530
|
+
exit(1)
|
413
531
|
end
|
414
532
|
end
|
415
533
|
|
@@ -422,33 +540,34 @@ module ClaudeSwarm
|
|
422
540
|
metadata = JSON.parse(File.read(session_metadata_file))
|
423
541
|
if metadata["worktree"] && metadata["worktree"]["enabled"]
|
424
542
|
worktree_name = metadata["worktree"]["name"]
|
425
|
-
say
|
543
|
+
say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
|
426
544
|
end
|
427
545
|
end
|
428
546
|
|
429
547
|
# Create orchestrator with restoration mode
|
430
548
|
generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
|
431
|
-
orchestrator = Orchestrator.new(
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
549
|
+
orchestrator = Orchestrator.new(
|
550
|
+
config,
|
551
|
+
generator,
|
552
|
+
vibe: options[:vibe],
|
553
|
+
prompt: options[:prompt],
|
554
|
+
stream_logs: options[:stream_logs],
|
555
|
+
debug: options[:debug],
|
556
|
+
restore_session_path: session_path,
|
557
|
+
worktree: worktree_name,
|
558
|
+
session_id: options[:session_id],
|
559
|
+
)
|
438
560
|
orchestrator.start
|
439
561
|
rescue StandardError => e
|
440
|
-
error
|
441
|
-
error
|
442
|
-
exit
|
562
|
+
error("Failed to restore session: #{e.message}")
|
563
|
+
error(e.backtrace.join("\n")) if options[:debug]
|
564
|
+
exit(1)
|
443
565
|
end
|
444
566
|
end
|
445
567
|
|
446
568
|
def find_session_path(session_id)
|
447
569
|
sessions_dir = File.expand_path("~/.claude-swarm/sessions")
|
448
570
|
|
449
|
-
# Check if it's a full path
|
450
|
-
return session_id if File.exist?(File.join(session_id, "config.yml"))
|
451
|
-
|
452
571
|
# Search for the session ID in all projects
|
453
572
|
Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
|
454
573
|
config_path = File.join(path, "config.yml")
|
@@ -518,8 +637,17 @@ module ClaudeSwarm
|
|
518
637
|
repo_path = repo_git_path.split("/.git/worktrees/").first
|
519
638
|
|
520
639
|
# Try to remove worktree via git
|
521
|
-
system!(
|
522
|
-
|
640
|
+
system!(
|
641
|
+
"git",
|
642
|
+
"-C",
|
643
|
+
repo_path,
|
644
|
+
"worktree",
|
645
|
+
"remove",
|
646
|
+
worktree_path,
|
647
|
+
"--force",
|
648
|
+
out: File::NULL,
|
649
|
+
err: File::NULL,
|
650
|
+
)
|
523
651
|
end
|
524
652
|
end
|
525
653
|
|
@@ -532,7 +660,7 @@ module ClaudeSwarm
|
|
532
660
|
cleaned += 1
|
533
661
|
end
|
534
662
|
rescue StandardError => e
|
535
|
-
say
|
663
|
+
say("Warning: Failed to clean worktree directory #{session_worktree_dir}: #{e.message}", :yellow) if options[:debug]
|
536
664
|
end
|
537
665
|
end
|
538
666
|
|