claude_swarm 0.1.16 ā 0.1.17
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 +18 -0
- data/README.md +96 -9
- data/claude-swarm.yml +18 -34
- data/examples/monitoring-demo.yml +26 -0
- data/examples/multi-directory.yml +26 -0
- data/lib/claude_swarm/claude_code_executor.rb +8 -1
- data/lib/claude_swarm/claude_mcp_server.rb +2 -1
- data/lib/claude_swarm/cli.rb +80 -1
- data/lib/claude_swarm/commands/ps.rb +148 -0
- data/lib/claude_swarm/commands/show.rb +158 -0
- data/lib/claude_swarm/configuration.rb +20 -3
- data/lib/claude_swarm/mcp_generator.rb +3 -0
- data/lib/claude_swarm/orchestrator.rb +57 -2
- data/lib/claude_swarm/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f0ab99665f31bddf2d3641525481ce2df55e910950af03290137246847bf70d
|
4
|
+
data.tar.gz: 2435170f5b61604c3131b1680f60ed8727795c58432806d9b61007813d4f5ce3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee74c188f6cc7fd31bb8705661f0913674bb687fd1eae6c2973728d2418f62bbad86054a89bd150965f39ec0bbd37ac04b2413dd22340311afd03cd5ec9d5424
|
7
|
+
data.tar.gz: a6c5fb7db4a8659619c9c7b5de000303ce91b046b85d635381f437adc92b4fd739e8d2142f0e65323346d1ba4d2607bf2b90c7d76fcf7c119bf6b9d5c431a7fc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
## [0.1.17]
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- **Multi-directory support**: Instances can now access multiple directories
|
5
|
+
- The `directory` field in YAML configuration now accepts either a string (single directory) or an array of strings (multiple directories)
|
6
|
+
- Additional directories are passed to Claude using the `--add-dir` flag
|
7
|
+
- The first directory in the array serves as the primary working directory
|
8
|
+
- All specified directories must exist or validation will fail
|
9
|
+
- Example: `directory: [./frontend, ./backend, ./shared]`
|
10
|
+
- **Session monitoring commands**: New commands for monitoring and managing active Claude Swarm sessions
|
11
|
+
- `claude-swarm ps`: List all active sessions with properly aligned columns showing session ID, swarm name, total cost, uptime, and directories
|
12
|
+
- `claude-swarm show SESSION_ID`: Display detailed session information including instance hierarchy and individual costs
|
13
|
+
- `claude-swarm watch SESSION_ID`: Tail session logs in real-time (uses native `tail -f`)
|
14
|
+
- `claude-swarm clean`: Remove stale session symlinks with optional age filtering (`--days N`)
|
15
|
+
- Active sessions are tracked via symlinks in `~/.claude-swarm/run/` for efficient monitoring
|
16
|
+
- Cost tracking aggregates data from `session.log.json` for accurate reporting
|
17
|
+
- Interactive main instance shows "n/a (interactive)" for cost when not available
|
18
|
+
|
1
19
|
## [0.1.16]
|
2
20
|
|
3
21
|
### Changed
|
data/README.md
CHANGED
@@ -203,8 +203,8 @@ Each instance must have:
|
|
203
203
|
|
204
204
|
Each instance can have:
|
205
205
|
|
206
|
-
- **directory**: Working directory for this instance (can use ~ for home)
|
207
|
-
- **model**: Claude model to use (opus, sonnet
|
206
|
+
- **directory**: Working directory for this instance (can use ~ for home). Can be a string for a single directory or an array of strings for multiple directories
|
207
|
+
- **model**: Claude model to use (opus, sonnet)
|
208
208
|
- **connections**: Array of other instances this one can communicate with
|
209
209
|
- **allowed_tools**: Array of tools this instance can use (backward compatible with `tools`)
|
210
210
|
- **disallowed_tools**: Array of tools to explicitly deny (takes precedence over allowed_tools)
|
@@ -336,7 +336,7 @@ swarm:
|
|
336
336
|
database:
|
337
337
|
description: "Database administrator managing data persistence"
|
338
338
|
directory: ./db
|
339
|
-
model:
|
339
|
+
model: sonnet
|
340
340
|
allowed_tools:
|
341
341
|
- Read
|
342
342
|
- Bash
|
@@ -398,6 +398,36 @@ swarm:
|
|
398
398
|
- Read
|
399
399
|
```
|
400
400
|
|
401
|
+
#### Multi-Directory Support
|
402
|
+
|
403
|
+
Instances can have access to multiple directories using an array (uses `claude --add-dir`):
|
404
|
+
|
405
|
+
```yaml
|
406
|
+
version: 1
|
407
|
+
swarm:
|
408
|
+
name: "Multi-Module Project"
|
409
|
+
main: fullstack_dev
|
410
|
+
instances:
|
411
|
+
fullstack_dev:
|
412
|
+
description: "Full-stack developer working across multiple modules"
|
413
|
+
directory: [./frontend, ./backend, ./shared] # Access to multiple directories
|
414
|
+
model: opus
|
415
|
+
allowed_tools: [Read, Edit, Write, Bash]
|
416
|
+
prompt: "You work across frontend, backend, and shared code modules"
|
417
|
+
|
418
|
+
documentation_writer:
|
419
|
+
description: "Documentation specialist with access to code and docs"
|
420
|
+
directory: ["./docs", "./src", "./examples"] # Multiple directories as array
|
421
|
+
model: sonnet
|
422
|
+
allowed_tools: [Read, Write, Edit]
|
423
|
+
prompt: "You maintain documentation based on code and examples"
|
424
|
+
```
|
425
|
+
|
426
|
+
When using multiple directories:
|
427
|
+
- The first directory in the array is the primary working directory
|
428
|
+
- Additional directories are accessible via the `--add-dir` flag in Claude
|
429
|
+
- All directories must exist or the configuration will fail validation
|
430
|
+
|
401
431
|
#### Mixed Permission Modes
|
402
432
|
|
403
433
|
You can have different permission modes for different instances:
|
@@ -450,10 +480,6 @@ claude-swarm --prompt "Fix the bug in the payment module"
|
|
450
480
|
claude-swarm --session-id 20241206_143022
|
451
481
|
claude-swarm --session-id ~/path/to/session
|
452
482
|
|
453
|
-
# List available sessions
|
454
|
-
claude-swarm list-sessions
|
455
|
-
claude-swarm list-sessions --limit 20
|
456
|
-
|
457
483
|
# Show version
|
458
484
|
claude-swarm version
|
459
485
|
|
@@ -464,6 +490,67 @@ claude-swarm version
|
|
464
490
|
claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TIMESTAMP
|
465
491
|
```
|
466
492
|
|
493
|
+
### Session Monitoring
|
494
|
+
|
495
|
+
Claude Swarm provides commands to monitor and inspect running sessions:
|
496
|
+
|
497
|
+
```bash
|
498
|
+
# List running swarm sessions with costs and uptime
|
499
|
+
claude-swarm ps
|
500
|
+
|
501
|
+
# Show detailed information about a session including instance hierarchy
|
502
|
+
claude-swarm show 20250617_235233
|
503
|
+
|
504
|
+
# Watch live logs from a session
|
505
|
+
claude-swarm watch 20250617_235233
|
506
|
+
|
507
|
+
# Watch logs starting from the last 50 lines
|
508
|
+
claude-swarm watch 20250617_235233 -n 50
|
509
|
+
|
510
|
+
# List all available sessions (including completed ones)
|
511
|
+
claude-swarm list-sessions
|
512
|
+
claude-swarm list-sessions --limit 20
|
513
|
+
|
514
|
+
# Clean up stale session symlinks
|
515
|
+
claude-swarm clean
|
516
|
+
|
517
|
+
# Remove sessions older than 30 days
|
518
|
+
claude-swarm clean --days 30
|
519
|
+
```
|
520
|
+
|
521
|
+
Example output from `claude-swarm ps`:
|
522
|
+
```
|
523
|
+
ā ļø Total cost does not include the cost of the main instance
|
524
|
+
|
525
|
+
SESSION_ID SWARM_NAME TOTAL_COST UPTIME DIRECTORY
|
526
|
+
-------------------------------------------------------------------------------
|
527
|
+
20250617_235233 Feature Development $0.3847 15m .
|
528
|
+
20250617_143022 Bug Investigation $1.2156 1h ./shopify
|
529
|
+
20250617_091547 Multi-Module Dev $0.8932 30m ./frontend, ./backend, ./shared
|
530
|
+
```
|
531
|
+
|
532
|
+
Note: The total cost shown reflects only the costs of connected instances called via MCP. The main instance cost is not tracked when running interactively.
|
533
|
+
|
534
|
+
Example output from `claude-swarm show`:
|
535
|
+
```
|
536
|
+
Session: 20250617_235233
|
537
|
+
Swarm: Feature Development
|
538
|
+
Total Cost: $0.3847 (excluding main instance)
|
539
|
+
Start Directory: /Users/paulo/project
|
540
|
+
|
541
|
+
Instance Hierarchy:
|
542
|
+
--------------------------------------------------
|
543
|
+
āā orchestrator [main] (orchestrator_e85036fc)
|
544
|
+
Cost: n/a (interactive) | Calls: 0
|
545
|
+
āā test_archaeologist (test_archaeologist_c504ca5f)
|
546
|
+
Cost: $0.1925 | Calls: 1
|
547
|
+
āā pr_analyst (pr_analyst_bfbefe56)
|
548
|
+
Cost: $0.1922 | Calls: 1
|
549
|
+
|
550
|
+
Note: Main instance (orchestrator) cost is not tracked in interactive mode.
|
551
|
+
View costs directly in the Claude interface.
|
552
|
+
```
|
553
|
+
|
467
554
|
### Session Management and Restoration (Experimental)
|
468
555
|
|
469
556
|
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.**
|
@@ -502,10 +589,10 @@ Resume a previous session with all instances restored to their Claude session st
|
|
502
589
|
|
503
590
|
```bash
|
504
591
|
# Resume by session ID
|
505
|
-
claude-swarm --session-id
|
592
|
+
claude-swarm --session-id 20250617_143022
|
506
593
|
|
507
594
|
# Resume by full path
|
508
|
-
claude-swarm --session-id ~/.claude-swarm/sessions/my-project/
|
595
|
+
claude-swarm --session-id ~/.claude-swarm/sessions/my-project/20250617_143022
|
509
596
|
```
|
510
597
|
|
511
598
|
This will:
|
data/claude-swarm.yml
CHANGED
@@ -1,42 +1,26 @@
|
|
1
1
|
version: 1
|
2
2
|
swarm:
|
3
3
|
name: "Swarm Name"
|
4
|
-
main:
|
4
|
+
main: claude_swarm_architect
|
5
5
|
instances:
|
6
|
-
|
7
|
-
description: "Lead
|
8
|
-
directory:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
claude_swarm_architect:
|
7
|
+
description: "Lead architect"
|
8
|
+
directory:
|
9
|
+
- .
|
10
|
+
- /Users/paulo/src/github.com/shopify-playground/claudeception
|
11
|
+
model: opus
|
12
|
+
prompt: "You are an expert in Claude swarm architecture"
|
13
|
+
vibe: true
|
14
|
+
connections: [claudeception_architect]
|
13
15
|
|
14
16
|
# Example instances (uncomment and modify as needed):
|
15
17
|
|
16
|
-
|
17
|
-
description: "
|
18
|
-
directory:
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
claudeception_architect:
|
19
|
+
description: "You are an expert in Claudeception architecture"
|
20
|
+
directory:
|
21
|
+
- .
|
22
|
+
- /Users/paulo/src/github.com/shopify-playground/claudeception
|
23
|
+
model: opus
|
24
|
+
prompt: "You are an expert in Claudeception architecture"
|
25
|
+
vibe: true
|
22
26
|
|
23
|
-
backend_dev:
|
24
|
-
description: "Backend developer focusing on APIs, databases, and server architecture"
|
25
|
-
directory: .
|
26
|
-
model: sonnet
|
27
|
-
prompt: "You specialize in backend development, APIs, databases, and server architecture"
|
28
|
-
allowed_tools: [Read, Edit, Write, Bash]
|
29
|
-
|
30
|
-
# devops_engineer:
|
31
|
-
# description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
|
32
|
-
# directory: .
|
33
|
-
# model: sonnet
|
34
|
-
# prompt: "You specialize in infrastructure, CI/CD, containerization, and deployment"
|
35
|
-
# allowed_tools: [Read, Edit, Write, Bash]
|
36
|
-
|
37
|
-
# qa_engineer:
|
38
|
-
# description: "QA engineer ensuring quality through comprehensive testing"
|
39
|
-
# directory: ./tests
|
40
|
-
# model: sonnet
|
41
|
-
# prompt: "You specialize in testing, quality assurance, and test automation"
|
42
|
-
# allowed_tools: [Read, Edit, Write, Bash]
|
@@ -0,0 +1,26 @@
|
|
1
|
+
version: 1
|
2
|
+
swarm:
|
3
|
+
name: "Monitoring Demo"
|
4
|
+
main: coordinator
|
5
|
+
instances:
|
6
|
+
coordinator:
|
7
|
+
description: "Main coordinator managing the team"
|
8
|
+
directory: .
|
9
|
+
model: haiku
|
10
|
+
connections: [analyzer, reporter]
|
11
|
+
prompt: "You coordinate analysis and reporting tasks"
|
12
|
+
allowed_tools: [Read, Edit]
|
13
|
+
|
14
|
+
analyzer:
|
15
|
+
description: "Data analyzer processing information"
|
16
|
+
directory: ./data
|
17
|
+
model: haiku
|
18
|
+
prompt: "You analyze data and provide insights"
|
19
|
+
allowed_tools: [Read, Bash]
|
20
|
+
|
21
|
+
reporter:
|
22
|
+
description: "Report generator creating summaries"
|
23
|
+
directory: ./reports
|
24
|
+
model: haiku
|
25
|
+
prompt: "You generate reports from analysis"
|
26
|
+
allowed_tools: [Write, Edit]
|
@@ -0,0 +1,26 @@
|
|
1
|
+
version: 1
|
2
|
+
swarm:
|
3
|
+
name: "Multi-Directory Example"
|
4
|
+
main: fullstack_dev
|
5
|
+
instances:
|
6
|
+
fullstack_dev:
|
7
|
+
description: "Full-stack developer with access to multiple project directories"
|
8
|
+
directory: [./frontend, ./backend, ./shared, ./docs]
|
9
|
+
model: opus
|
10
|
+
connections: [frontend_specialist, backend_specialist]
|
11
|
+
allowed_tools: [Read, Edit, Write, Bash, WebSearch]
|
12
|
+
prompt: "You are a full-stack developer with access to frontend, backend, shared code, and documentation directories"
|
13
|
+
|
14
|
+
frontend_specialist:
|
15
|
+
description: "Frontend developer focused on React components"
|
16
|
+
directory: ./frontend
|
17
|
+
model: sonnet
|
18
|
+
allowed_tools: [Read, Edit, Write, Bash]
|
19
|
+
prompt: "You specialize in React and frontend development"
|
20
|
+
|
21
|
+
backend_specialist:
|
22
|
+
description: "Backend developer focused on API development"
|
23
|
+
directory: ./backend
|
24
|
+
model: sonnet
|
25
|
+
allowed_tools: [Read, Edit, Write, Bash]
|
26
|
+
prompt: "You specialize in backend API development"
|
@@ -12,8 +12,9 @@ module ClaudeSwarm
|
|
12
12
|
|
13
13
|
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
|
14
14
|
instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
|
15
|
-
claude_session_id: nil)
|
15
|
+
claude_session_id: nil, additional_directories: [])
|
16
16
|
@working_directory = working_directory
|
17
|
+
@additional_directories = additional_directories
|
17
18
|
@model = model
|
18
19
|
@mcp_config = mcp_config
|
19
20
|
@vibe = vibe
|
@@ -259,6 +260,12 @@ module ClaudeSwarm
|
|
259
260
|
|
260
261
|
cmd_array << "--verbose"
|
261
262
|
|
263
|
+
# Add additional directories with --add-dir
|
264
|
+
cmd_array << "--add-dir" if @additional_directories.any?
|
265
|
+
@additional_directories.each do |additional_dir|
|
266
|
+
cmd_array << additional_dir
|
267
|
+
end
|
268
|
+
|
262
269
|
# Add MCP config if specified
|
263
270
|
cmd_array += ["--mcp-config", @mcp_config] if @mcp_config
|
264
271
|
|
@@ -28,7 +28,8 @@ module ClaudeSwarm
|
|
28
28
|
instance_id: instance_config[:instance_id],
|
29
29
|
calling_instance: calling_instance,
|
30
30
|
calling_instance_id: calling_instance_id,
|
31
|
-
claude_session_id: instance_config[:claude_session_id]
|
31
|
+
claude_session_id: instance_config[:claude_session_id],
|
32
|
+
additional_directories: instance_config[:directories][1..] || []
|
32
33
|
)
|
33
34
|
|
34
35
|
# Set class variables so tools can access them
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -71,6 +71,8 @@ module ClaudeSwarm
|
|
71
71
|
desc: "Instance name"
|
72
72
|
method_option :directory, aliases: "-d", type: :string, required: true,
|
73
73
|
desc: "Working directory for the instance"
|
74
|
+
method_option :directories, type: :array,
|
75
|
+
desc: "All directories (including main directory) for the instance"
|
74
76
|
method_option :model, aliases: "-m", type: :string, required: true,
|
75
77
|
desc: "Claude model to use (e.g., opus, sonnet)"
|
76
78
|
method_option :prompt, aliases: "-p", type: :string,
|
@@ -101,6 +103,7 @@ module ClaudeSwarm
|
|
101
103
|
instance_config = {
|
102
104
|
name: options[:name],
|
103
105
|
directory: options[:directory],
|
106
|
+
directories: options[:directories] || [options[:directory]],
|
104
107
|
model: options[:model],
|
105
108
|
prompt: options[:prompt],
|
106
109
|
description: options[:description],
|
@@ -108,7 +111,7 @@ module ClaudeSwarm
|
|
108
111
|
disallowed_tools: options[:disallowed_tools] || [],
|
109
112
|
connections: options[:connections] || [],
|
110
113
|
mcp_config_path: options[:mcp_config_path],
|
111
|
-
vibe: options[:vibe],
|
114
|
+
vibe: options[:vibe] || false,
|
112
115
|
instance_id: options[:instance_id],
|
113
116
|
claude_session_id: options[:claude_session_id]
|
114
117
|
}
|
@@ -194,6 +197,82 @@ module ClaudeSwarm
|
|
194
197
|
say "Claude Swarm #{VERSION}"
|
195
198
|
end
|
196
199
|
|
200
|
+
desc "ps", "List running Claude Swarm sessions"
|
201
|
+
def ps
|
202
|
+
require_relative "commands/ps"
|
203
|
+
Commands::Ps.new.execute
|
204
|
+
end
|
205
|
+
|
206
|
+
desc "show SESSION_ID", "Show detailed session information"
|
207
|
+
def show(session_id)
|
208
|
+
require_relative "commands/show"
|
209
|
+
Commands::Show.new.execute(session_id)
|
210
|
+
end
|
211
|
+
|
212
|
+
desc "clean", "Remove stale session symlinks"
|
213
|
+
method_option :days, aliases: "-d", type: :numeric, default: 7,
|
214
|
+
desc: "Remove sessions older than N days"
|
215
|
+
def clean
|
216
|
+
run_dir = File.expand_path("~/.claude-swarm/run")
|
217
|
+
unless Dir.exist?(run_dir)
|
218
|
+
say "No run directory found", :yellow
|
219
|
+
return
|
220
|
+
end
|
221
|
+
|
222
|
+
cleaned = 0
|
223
|
+
Dir.glob("#{run_dir}/*").each do |symlink|
|
224
|
+
next unless File.symlink?(symlink)
|
225
|
+
|
226
|
+
begin
|
227
|
+
# Remove if target doesn't exist (stale)
|
228
|
+
unless File.exist?(File.readlink(symlink))
|
229
|
+
File.unlink(symlink)
|
230
|
+
cleaned += 1
|
231
|
+
next
|
232
|
+
end
|
233
|
+
|
234
|
+
# Remove if older than specified days
|
235
|
+
if File.stat(symlink).mtime < Time.now - (options[:days] * 86_400)
|
236
|
+
File.unlink(symlink)
|
237
|
+
cleaned += 1
|
238
|
+
end
|
239
|
+
rescue StandardError
|
240
|
+
# Skip problematic symlinks
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
say "Cleaned #{cleaned} stale session#{cleaned == 1 ? "" : "s"}", :green
|
245
|
+
end
|
246
|
+
|
247
|
+
desc "watch SESSION_ID", "Watch session logs"
|
248
|
+
method_option :lines, aliases: "-n", type: :numeric, default: 100,
|
249
|
+
desc: "Number of lines to show initially"
|
250
|
+
def watch(session_id)
|
251
|
+
# Find session path
|
252
|
+
run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
|
253
|
+
session_path = if File.symlink?(run_symlink)
|
254
|
+
File.readlink(run_symlink)
|
255
|
+
else
|
256
|
+
# Search in sessions directory
|
257
|
+
Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
|
258
|
+
File.basename(path) == session_id
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
unless session_path && Dir.exist?(session_path)
|
263
|
+
error "Session not found: #{session_id}"
|
264
|
+
exit 1
|
265
|
+
end
|
266
|
+
|
267
|
+
log_file = File.join(session_path, "session.log")
|
268
|
+
unless File.exist?(log_file)
|
269
|
+
error "Log file not found for session: #{session_id}"
|
270
|
+
exit 1
|
271
|
+
end
|
272
|
+
|
273
|
+
exec("tail", "-f", "-n", options[:lines].to_s, log_file)
|
274
|
+
end
|
275
|
+
|
197
276
|
desc "list-sessions", "List all available Claude Swarm sessions"
|
198
277
|
method_option :limit, aliases: "-l", type: :numeric, default: 10,
|
199
278
|
desc: "Maximum number of sessions to display"
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
|
7
|
+
module ClaudeSwarm
|
8
|
+
module Commands
|
9
|
+
class Ps
|
10
|
+
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
11
|
+
|
12
|
+
def execute
|
13
|
+
unless Dir.exist?(RUN_DIR)
|
14
|
+
puts "No active sessions"
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
sessions = []
|
19
|
+
|
20
|
+
# Read all symlinks in run directory
|
21
|
+
Dir.glob("#{RUN_DIR}/*").each do |symlink|
|
22
|
+
next unless File.symlink?(symlink)
|
23
|
+
|
24
|
+
begin
|
25
|
+
session_dir = File.readlink(symlink)
|
26
|
+
# Skip if target doesn't exist (stale symlink)
|
27
|
+
next unless Dir.exist?(session_dir)
|
28
|
+
|
29
|
+
session_info = parse_session_info(session_dir)
|
30
|
+
sessions << session_info if session_info
|
31
|
+
rescue StandardError
|
32
|
+
# Skip problematic symlinks
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if sessions.empty?
|
37
|
+
puts "No active sessions"
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
# Column widths
|
42
|
+
col_session = 15
|
43
|
+
col_swarm = 25
|
44
|
+
col_cost = 12
|
45
|
+
col_uptime = 10
|
46
|
+
|
47
|
+
# Display header with proper spacing
|
48
|
+
header = "#{
|
49
|
+
"SESSION_ID".ljust(col_session)
|
50
|
+
} #{
|
51
|
+
"SWARM_NAME".ljust(col_swarm)
|
52
|
+
} #{
|
53
|
+
"TOTAL_COST".ljust(col_cost)
|
54
|
+
} #{
|
55
|
+
"UPTIME".ljust(col_uptime)
|
56
|
+
} DIRECTORY"
|
57
|
+
puts "\nā ļø \e[3mTotal cost does not include the cost of the main instance\e[0m\n\n"
|
58
|
+
puts header
|
59
|
+
puts "-" * header.length
|
60
|
+
|
61
|
+
# Display sessions sorted by start time (newest first)
|
62
|
+
sessions.sort_by { |s| s[:start_time] }.reverse.each do |session|
|
63
|
+
cost_str = format("$%.4f", session[:cost])
|
64
|
+
puts "#{
|
65
|
+
session[:id].ljust(col_session)
|
66
|
+
} #{
|
67
|
+
truncate(session[:name], col_swarm).ljust(col_swarm)
|
68
|
+
} #{
|
69
|
+
cost_str.ljust(col_cost)
|
70
|
+
} #{
|
71
|
+
session[:uptime].ljust(col_uptime)
|
72
|
+
} #{session[:directory]}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse_session_info(session_dir)
|
79
|
+
session_id = File.basename(session_dir)
|
80
|
+
|
81
|
+
# Load config for swarm name and main directory
|
82
|
+
config_file = File.join(session_dir, "config.yml")
|
83
|
+
return nil unless File.exist?(config_file)
|
84
|
+
|
85
|
+
config = YAML.load_file(config_file)
|
86
|
+
swarm_name = config.dig("swarm", "name") || "Unknown"
|
87
|
+
main_instance = config.dig("swarm", "main")
|
88
|
+
|
89
|
+
# Get all directories - handle both string and array formats
|
90
|
+
dir_config = config.dig("swarm", "instances", main_instance, "directory")
|
91
|
+
directories = if dir_config.is_a?(Array)
|
92
|
+
dir_config
|
93
|
+
else
|
94
|
+
[dir_config || "."]
|
95
|
+
end
|
96
|
+
directories_str = directories.join(", ")
|
97
|
+
|
98
|
+
# Calculate total cost from JSON log
|
99
|
+
total_cost = calculate_total_cost(session_dir)
|
100
|
+
|
101
|
+
# Get uptime from directory creation time
|
102
|
+
start_time = File.stat(session_dir).ctime
|
103
|
+
uptime = format_duration(Time.now - start_time)
|
104
|
+
|
105
|
+
{
|
106
|
+
id: session_id,
|
107
|
+
name: swarm_name,
|
108
|
+
cost: total_cost,
|
109
|
+
uptime: uptime,
|
110
|
+
directory: directories_str,
|
111
|
+
start_time: start_time
|
112
|
+
}
|
113
|
+
rescue StandardError
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def calculate_total_cost(session_dir)
|
118
|
+
log_file = File.join(session_dir, "session.log.json")
|
119
|
+
return 0.0 unless File.exist?(log_file)
|
120
|
+
|
121
|
+
total = 0.0
|
122
|
+
File.foreach(log_file) do |line|
|
123
|
+
data = JSON.parse(line)
|
124
|
+
total += data["event"]["total_cost_usd"] if data.dig("event", "type") == "result" && data.dig("event", "total_cost_usd")
|
125
|
+
rescue JSON::ParserError
|
126
|
+
next
|
127
|
+
end
|
128
|
+
total
|
129
|
+
end
|
130
|
+
|
131
|
+
def format_duration(seconds)
|
132
|
+
if seconds < 60
|
133
|
+
"#{seconds.to_i}s"
|
134
|
+
elsif seconds < 3600
|
135
|
+
"#{(seconds / 60).to_i}m"
|
136
|
+
elsif seconds < 86_400
|
137
|
+
"#{(seconds / 3600).to_i}h"
|
138
|
+
else
|
139
|
+
"#{(seconds / 86_400).to_i}d"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def truncate(str, length)
|
144
|
+
str.length > length ? "#{str[0...length - 2]}.." : str
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module ClaudeSwarm
|
7
|
+
module Commands
|
8
|
+
class Show
|
9
|
+
def execute(session_id)
|
10
|
+
session_path = find_session_path(session_id)
|
11
|
+
unless session_path
|
12
|
+
puts "Session not found: #{session_id}"
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
# Load config to get main instance name
|
17
|
+
config = YAML.load_file(File.join(session_path, "config.yml"))
|
18
|
+
main_instance_name = config.dig("swarm", "main")
|
19
|
+
|
20
|
+
# Parse all events to build instance data
|
21
|
+
instances = parse_instance_hierarchy(session_path, main_instance_name)
|
22
|
+
|
23
|
+
# Calculate total cost (excluding main if not available)
|
24
|
+
total_cost = instances.values.sum { |i| i[:cost] }
|
25
|
+
cost_display = if instances[main_instance_name] && instances[main_instance_name][:has_cost_data]
|
26
|
+
format("$%.4f", total_cost)
|
27
|
+
else
|
28
|
+
"#{format("$%.4f", total_cost)} (excluding main instance)"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Display session info
|
32
|
+
puts "Session: #{session_id}"
|
33
|
+
puts "Swarm: #{config.dig("swarm", "name")}"
|
34
|
+
puts "Total Cost: #{cost_display}"
|
35
|
+
|
36
|
+
# Try to read start directory
|
37
|
+
start_dir_file = File.join(session_path, "start_directory")
|
38
|
+
puts "Start Directory: #{File.read(start_dir_file).strip}" if File.exist?(start_dir_file)
|
39
|
+
|
40
|
+
puts
|
41
|
+
puts "Instance Hierarchy:"
|
42
|
+
puts "-" * 50
|
43
|
+
|
44
|
+
# Find root instances
|
45
|
+
roots = instances.values.select { |i| i[:called_by].empty? }
|
46
|
+
roots.each do |instance|
|
47
|
+
display_instance_tree(instance, instances, 0, main_instance_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add note about interactive main instance
|
51
|
+
return if instances[main_instance_name]&.dig(:has_cost_data)
|
52
|
+
|
53
|
+
puts
|
54
|
+
puts "Note: Main instance (#{main_instance_name}) cost is not tracked in interactive mode."
|
55
|
+
puts " View costs directly in the Claude interface."
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def find_session_path(session_id)
|
61
|
+
# First check the run directory
|
62
|
+
run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
|
63
|
+
if File.symlink?(run_symlink)
|
64
|
+
target = File.readlink(run_symlink)
|
65
|
+
return target if Dir.exist?(target)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Fall back to searching all sessions
|
69
|
+
Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
|
70
|
+
File.basename(path) == session_id
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_instance_hierarchy(session_path, _main_instance_name)
|
75
|
+
log_file = File.join(session_path, "session.log.json")
|
76
|
+
instances = {}
|
77
|
+
|
78
|
+
return instances unless File.exist?(log_file)
|
79
|
+
|
80
|
+
File.foreach(log_file) do |line|
|
81
|
+
data = JSON.parse(line)
|
82
|
+
instance_name = data["instance"]
|
83
|
+
instance_id = data["instance_id"]
|
84
|
+
calling_instance = data["calling_instance"]
|
85
|
+
|
86
|
+
# Initialize instance data
|
87
|
+
instances[instance_name] ||= {
|
88
|
+
name: instance_name,
|
89
|
+
id: instance_id,
|
90
|
+
cost: 0.0,
|
91
|
+
calls: 0,
|
92
|
+
called_by: Set.new,
|
93
|
+
calls_to: Set.new,
|
94
|
+
has_cost_data: false
|
95
|
+
}
|
96
|
+
|
97
|
+
# Track relationships
|
98
|
+
if calling_instance && calling_instance != instance_name
|
99
|
+
instances[instance_name][:called_by] << calling_instance
|
100
|
+
|
101
|
+
instances[calling_instance] ||= {
|
102
|
+
name: calling_instance,
|
103
|
+
id: data["calling_instance_id"],
|
104
|
+
cost: 0.0,
|
105
|
+
calls: 0,
|
106
|
+
called_by: Set.new,
|
107
|
+
calls_to: Set.new,
|
108
|
+
has_cost_data: false
|
109
|
+
}
|
110
|
+
instances[calling_instance][:calls_to] << instance_name
|
111
|
+
end
|
112
|
+
|
113
|
+
# Track costs and calls
|
114
|
+
if data.dig("event", "type") == "result"
|
115
|
+
instances[instance_name][:calls] += 1
|
116
|
+
if (cost = data.dig("event", "total_cost_usd"))
|
117
|
+
instances[instance_name][:cost] += cost
|
118
|
+
instances[instance_name][:has_cost_data] = true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
rescue JSON::ParserError
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
instances
|
126
|
+
end
|
127
|
+
|
128
|
+
def display_instance_tree(instance, all_instances, level, main_instance_name)
|
129
|
+
indent = " " * level
|
130
|
+
prefix = level.zero? ? "āā" : "āā"
|
131
|
+
|
132
|
+
# Display instance name with special marker for main
|
133
|
+
instance_display = instance[:name]
|
134
|
+
instance_display += " [main]" if instance[:name] == main_instance_name
|
135
|
+
|
136
|
+
puts "#{indent}#{prefix} #{instance_display} (#{instance[:id]})"
|
137
|
+
|
138
|
+
# Display cost - show n/a for main instance without cost data
|
139
|
+
cost_display = if instance[:name] == main_instance_name && !instance[:has_cost_data]
|
140
|
+
"n/a (interactive)"
|
141
|
+
else
|
142
|
+
format("$%.4f", instance[:cost])
|
143
|
+
end
|
144
|
+
|
145
|
+
puts "#{indent} Cost: #{cost_display} | Calls: #{instance[:calls]}"
|
146
|
+
|
147
|
+
# Display children
|
148
|
+
children = instance[:calls_to].map { |name| all_instances[name] }.compact
|
149
|
+
children.each do |child|
|
150
|
+
# Don't recurse if we've already shown this instance (avoid cycles)
|
151
|
+
next if level.positive? && child[:called_by].size > 1
|
152
|
+
|
153
|
+
display_instance_tree(child, all_instances, level + 1, main_instance_name)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -86,9 +86,13 @@ module ClaudeSwarm
|
|
86
86
|
# Support both 'tools' (deprecated) and 'allowed_tools' for backward compatibility
|
87
87
|
allowed_tools = config["allowed_tools"] || config["tools"] || []
|
88
88
|
|
89
|
+
# Parse directory field - support both string and array
|
90
|
+
directories = parse_directories(config["directory"])
|
91
|
+
|
89
92
|
{
|
90
93
|
name: name,
|
91
|
-
directory:
|
94
|
+
directory: directories.first, # Keep single directory for backward compatibility
|
95
|
+
directories: directories, # New field with all directories
|
92
96
|
model: config["model"] || "sonnet",
|
93
97
|
connections: Array(config["connections"]),
|
94
98
|
tools: Array(allowed_tools), # Keep as 'tools' internally for compatibility
|
@@ -156,8 +160,10 @@ module ClaudeSwarm
|
|
156
160
|
|
157
161
|
def validate_directories
|
158
162
|
@instances.each do |name, instance|
|
159
|
-
|
160
|
-
|
163
|
+
# Validate all directories in the directories array
|
164
|
+
instance[:directories].each do |directory|
|
165
|
+
raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
|
166
|
+
end
|
161
167
|
end
|
162
168
|
end
|
163
169
|
|
@@ -168,6 +174,17 @@ module ClaudeSwarm
|
|
168
174
|
raise Error, "Instance '#{instance_name}' field '#{field_name}' must be an array, got #{field_value.class.name}" unless field_value.is_a?(Array)
|
169
175
|
end
|
170
176
|
|
177
|
+
def parse_directories(directory_config)
|
178
|
+
# Default to current directory if not specified
|
179
|
+
directory_config ||= "."
|
180
|
+
|
181
|
+
# Convert to array and expand paths
|
182
|
+
directories = Array(directory_config).map { |dir| expand_path(dir) }
|
183
|
+
|
184
|
+
# Ensure at least one directory
|
185
|
+
directories.empty? ? [expand_path(".")] : directories
|
186
|
+
end
|
187
|
+
|
171
188
|
def expand_path(path)
|
172
189
|
Pathname.new(path).expand_path(@base_dir).to_s
|
173
190
|
end
|
@@ -107,6 +107,9 @@ module ClaudeSwarm
|
|
107
107
|
"--model", instance[:model]
|
108
108
|
]
|
109
109
|
|
110
|
+
# Add directories array if we have multiple directories
|
111
|
+
args.push("--directories", *instance[:directories]) if instance[:directories] && instance[:directories].size > 1
|
112
|
+
|
110
113
|
# Add optional arguments
|
111
114
|
args.push("--prompt", instance[:prompt]) if instance[:prompt]
|
112
115
|
|
@@ -8,6 +8,8 @@ require_relative "process_tracker"
|
|
8
8
|
|
9
9
|
module ClaudeSwarm
|
10
10
|
class Orchestrator
|
11
|
+
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
12
|
+
|
11
13
|
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
|
12
14
|
restore_session_path: nil)
|
13
15
|
@config = configuration
|
@@ -17,6 +19,7 @@ module ClaudeSwarm
|
|
17
19
|
@stream_logs = stream_logs
|
18
20
|
@debug = debug
|
19
21
|
@restore_session_path = restore_session_path
|
22
|
+
@session_path = nil
|
20
23
|
end
|
21
24
|
|
22
25
|
def start
|
@@ -29,9 +32,13 @@ module ClaudeSwarm
|
|
29
32
|
|
30
33
|
# Use existing session path
|
31
34
|
session_path = @restore_session_path
|
35
|
+
@session_path = session_path
|
32
36
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
33
37
|
ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
|
34
38
|
|
39
|
+
# Create run symlink for restored session
|
40
|
+
create_run_symlink
|
41
|
+
|
35
42
|
unless @prompt
|
36
43
|
puts "š Using existing session: #{session_path}/"
|
37
44
|
puts
|
@@ -59,10 +66,14 @@ module ClaudeSwarm
|
|
59
66
|
# Generate and set session path for all instances
|
60
67
|
session_path = SessionPath.generate(working_dir: Dir.pwd)
|
61
68
|
SessionPath.ensure_directory(session_path)
|
69
|
+
@session_path = session_path
|
62
70
|
|
63
71
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
64
72
|
ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
|
65
73
|
|
74
|
+
# Create run symlink for new session
|
75
|
+
create_run_symlink
|
76
|
+
|
66
77
|
unless @prompt
|
67
78
|
puts "š Session files will be saved to: #{session_path}/"
|
68
79
|
puts
|
@@ -90,7 +101,12 @@ module ClaudeSwarm
|
|
90
101
|
unless @prompt
|
91
102
|
puts "š Launching main instance: #{@config.main_instance}"
|
92
103
|
puts " Model: #{main_instance[:model]}"
|
93
|
-
|
104
|
+
if main_instance[:directories].size == 1
|
105
|
+
puts " Directory: #{main_instance[:directory]}"
|
106
|
+
else
|
107
|
+
puts " Directories:"
|
108
|
+
main_instance[:directories].each { |dir| puts " - #{dir}" }
|
109
|
+
end
|
94
110
|
puts " Allowed tools: #{main_instance[:allowed_tools].join(", ")}" if main_instance[:allowed_tools].any?
|
95
111
|
puts " Disallowed tools: #{main_instance[:disallowed_tools].join(", ")}" if main_instance[:disallowed_tools]&.any?
|
96
112
|
puts " Connections: #{main_instance[:connections].join(", ")}" if main_instance[:connections].any?
|
@@ -119,8 +135,9 @@ module ClaudeSwarm
|
|
119
135
|
log_thread.join
|
120
136
|
end
|
121
137
|
|
122
|
-
# Clean up child processes
|
138
|
+
# Clean up child processes and run symlink
|
123
139
|
cleanup_processes
|
140
|
+
cleanup_run_symlink
|
124
141
|
end
|
125
142
|
|
126
143
|
private
|
@@ -140,6 +157,7 @@ module ClaudeSwarm
|
|
140
157
|
Signal.trap(signal) do
|
141
158
|
puts "\nš Received #{signal} signal, cleaning up..."
|
142
159
|
cleanup_processes
|
160
|
+
cleanup_run_symlink
|
143
161
|
exit
|
144
162
|
end
|
145
163
|
end
|
@@ -152,6 +170,35 @@ module ClaudeSwarm
|
|
152
170
|
puts "ā ļø Error during cleanup: #{e.message}"
|
153
171
|
end
|
154
172
|
|
173
|
+
def create_run_symlink
|
174
|
+
return unless @session_path
|
175
|
+
|
176
|
+
FileUtils.mkdir_p(RUN_DIR)
|
177
|
+
|
178
|
+
# Session ID is the last part of the session path
|
179
|
+
session_id = File.basename(@session_path)
|
180
|
+
symlink_path = File.join(RUN_DIR, session_id)
|
181
|
+
|
182
|
+
# Remove stale symlink if exists
|
183
|
+
File.unlink(symlink_path) if File.symlink?(symlink_path)
|
184
|
+
|
185
|
+
# Create new symlink
|
186
|
+
File.symlink(@session_path, symlink_path)
|
187
|
+
rescue StandardError => e
|
188
|
+
# Don't fail the process if symlink creation fails
|
189
|
+
puts "ā ļø Warning: Could not create run symlink: #{e.message}" unless @prompt
|
190
|
+
end
|
191
|
+
|
192
|
+
def cleanup_run_symlink
|
193
|
+
return unless @session_path
|
194
|
+
|
195
|
+
session_id = File.basename(@session_path)
|
196
|
+
symlink_path = File.join(RUN_DIR, session_id)
|
197
|
+
File.unlink(symlink_path) if File.symlink?(symlink_path)
|
198
|
+
rescue StandardError
|
199
|
+
# Ignore errors during cleanup
|
200
|
+
end
|
201
|
+
|
155
202
|
def start_log_streaming
|
156
203
|
Thread.new do
|
157
204
|
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
@@ -238,6 +285,14 @@ module ClaudeSwarm
|
|
238
285
|
|
239
286
|
parts << "--debug" if @debug
|
240
287
|
|
288
|
+
# Add additional directories with --add-dir
|
289
|
+
if instance[:directories].size > 1
|
290
|
+
instance[:directories][1..].each do |additional_dir|
|
291
|
+
parts << "--add-dir"
|
292
|
+
parts << additional_dir
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
241
296
|
mcp_config_path = @generator.mcp_config_path(@config.main_instance)
|
242
297
|
parts << "--mcp-config"
|
243
298
|
parts << mcp_config_path
|
data/lib/claude_swarm/version.rb
CHANGED
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.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paulo Arruda
|
@@ -65,11 +65,15 @@ files:
|
|
65
65
|
- example/microservices-team.yml
|
66
66
|
- example/session-restoration-demo.yml
|
67
67
|
- example/test-generation.yml
|
68
|
+
- examples/monitoring-demo.yml
|
69
|
+
- examples/multi-directory.yml
|
68
70
|
- exe/claude-swarm
|
69
71
|
- lib/claude_swarm.rb
|
70
72
|
- lib/claude_swarm/claude_code_executor.rb
|
71
73
|
- lib/claude_swarm/claude_mcp_server.rb
|
72
74
|
- lib/claude_swarm/cli.rb
|
75
|
+
- lib/claude_swarm/commands/ps.rb
|
76
|
+
- lib/claude_swarm/commands/show.rb
|
73
77
|
- lib/claude_swarm/configuration.rb
|
74
78
|
- lib/claude_swarm/mcp_generator.rb
|
75
79
|
- lib/claude_swarm/orchestrator.rb
|