swarm_memory 2.1.1 → 2.1.3

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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/cli.rb +9 -11
  3. data/lib/claude_swarm/commands/ps.rb +1 -2
  4. data/lib/claude_swarm/configuration.rb +30 -7
  5. data/lib/claude_swarm/mcp_generator.rb +4 -10
  6. data/lib/claude_swarm/orchestrator.rb +43 -44
  7. data/lib/claude_swarm/system_utils.rb +4 -4
  8. data/lib/claude_swarm/version.rb +1 -1
  9. data/lib/claude_swarm.rb +5 -9
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
  12. data/lib/swarm_cli/config_loader.rb +14 -13
  13. data/lib/swarm_cli/version.rb +1 -1
  14. data/lib/swarm_cli.rb +2 -0
  15. data/lib/swarm_memory/adapters/base.rb +4 -4
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
  17. data/lib/swarm_memory/core/storage.rb +66 -6
  18. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  19. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  20. data/lib/swarm_memory/integration/sdk_plugin.rb +24 -4
  21. data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
  22. data/lib/swarm_memory/tools/memory_edit.rb +3 -2
  23. data/lib/swarm_memory/tools/memory_glob.rb +24 -1
  24. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  25. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  26. data/lib/swarm_memory/tools/memory_write.rb +2 -2
  27. data/lib/swarm_memory/version.rb +1 -1
  28. data/lib/swarm_memory.rb +7 -0
  29. data/lib/swarm_sdk/agent/builder.rb +33 -0
  30. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  31. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  32. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  33. data/lib/swarm_sdk/agent/chat.rb +199 -52
  34. data/lib/swarm_sdk/agent/context.rb +6 -2
  35. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  36. data/lib/swarm_sdk/agent/definition.rb +32 -23
  37. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  38. data/lib/swarm_sdk/configuration.rb +420 -103
  39. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  40. data/lib/swarm_sdk/log_collector.rb +31 -5
  41. data/lib/swarm_sdk/log_stream.rb +37 -8
  42. data/lib/swarm_sdk/model_aliases.json +4 -1
  43. data/lib/swarm_sdk/node/agent_config.rb +39 -9
  44. data/lib/swarm_sdk/node/builder.rb +158 -42
  45. data/lib/swarm_sdk/node_context.rb +75 -0
  46. data/lib/swarm_sdk/node_orchestrator.rb +492 -18
  47. data/lib/swarm_sdk/plugin.rb +73 -1
  48. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  49. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  50. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  51. data/lib/swarm_sdk/restore_result.rb +65 -0
  52. data/lib/swarm_sdk/result.rb +32 -6
  53. data/lib/swarm_sdk/snapshot.rb +156 -0
  54. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  55. data/lib/swarm_sdk/state_restorer.rb +491 -0
  56. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  57. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  58. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  59. data/lib/swarm_sdk/swarm/builder.rb +208 -11
  60. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  61. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  62. data/lib/swarm_sdk/swarm.rb +367 -90
  63. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  64. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  65. data/lib/swarm_sdk/tools/delegate.rb +94 -9
  66. data/lib/swarm_sdk/tools/read.rb +17 -5
  67. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  68. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  69. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  70. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  71. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  72. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  73. data/lib/swarm_sdk/tools/think.rb +4 -1
  74. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  75. data/lib/swarm_sdk/utils.rb +18 -0
  76. data/lib/swarm_sdk/validation_result.rb +33 -0
  77. data/lib/swarm_sdk/version.rb +1 -1
  78. data/lib/swarm_sdk.rb +365 -28
  79. metadata +17 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38d427a70039bc09a7a1bbad5a0f2c9f28e46d80a954587ab27f04ad33c66e2f
4
- data.tar.gz: 3942488b1873520a984493c6270085b0f6f2e0e01560bea349903463d01bda11
3
+ metadata.gz: 8422c40d79fb0c2adcd9e1de9a4cfdf289ced49ef27507969ea797c4187a0ee1
4
+ data.tar.gz: 875f1429c37f2485d32641a7d96c9cae5db226a5bc2fc3e2f8c1443946e021b9
5
5
  SHA512:
6
- metadata.gz: 945735c0d60be12edc1452ea84059f6a3d6aa68966e687a81d2c8ccbd2492c5b2bdd9099fc4a14c40aaabd1d275cec0a80b35be8c9f5354d217467136493ec57
7
- data.tar.gz: f5b0680131c7d38ff229da49031628356c44c4edcb90685b7489e2bc2b30a6d361babf55d3e995d5f28acc62f56b28deaa03829d512f95550dd198b192179df0
6
+ metadata.gz: 298c0506a2f486aab1ad29c9f6e4605fe3fbb91fb17f7e3ee913b046f97a9d347312afd686e4bf26d28d6329c8974d421d39ee488e04bf2bc4cde22fea8cdc8c
7
+ data.tar.gz: c472ec8754b18a2a8d0ab1ae1a0dc391b2f4fffc2de670831272938d81e51a6716c81343b174afa8833d256e28a728fdd198e0bb2b6d92e13adc085c786d2abc
@@ -43,9 +43,8 @@ module ClaudeSwarm
43
43
  type: :string,
44
44
  desc: "Root directory for resolving relative paths (defaults to current directory)"
45
45
  def start(config_file = nil)
46
- # Set root directory early so it's available to all components
47
- root_dir = options[:root_dir] || Dir.pwd
48
- ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
46
+ # Determine root directory for this session
47
+ root_dir = File.expand_path(options[:root_dir] || Dir.pwd)
49
48
 
50
49
  # Resolve config path relative to root directory
51
50
  config_path = config_file || "claude-swarm.yml"
@@ -71,7 +70,7 @@ module ClaudeSwarm
71
70
  end
72
71
 
73
72
  begin
74
- config = Configuration.new(config_path, base_dir: ClaudeSwarm.root_dir, options: options)
73
+ config = Configuration.new(config_path, base_dir: root_dir, options: options)
75
74
  generator = McpGenerator.new(config, vibe: options[:vibe])
76
75
  orchestrator = Orchestrator.new(
77
76
  config,
@@ -547,24 +546,23 @@ module ClaudeSwarm
547
546
  exit(1)
548
547
  end
549
548
 
550
- # Change to the original root directory if it exists
549
+ # Load the original root directory from session
551
550
  root_dir_file = File.join(session_path, "root_directory")
552
- if File.exist?(root_dir_file)
551
+ root_dir = if File.exist?(root_dir_file)
553
552
  original_dir = File.read(root_dir_file).strip
554
553
  if Dir.exist?(original_dir)
555
- Dir.chdir(original_dir)
556
- ENV["CLAUDE_SWARM_ROOT_DIR"] = original_dir
557
- say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
554
+ say("Using original directory: #{original_dir}", :green) unless options[:prompt]
555
+ original_dir
558
556
  else
559
557
  error("Original directory no longer exists: #{original_dir}")
560
558
  exit(1)
561
559
  end
562
560
  else
563
561
  # If no root_directory file, use current directory
564
- ENV["CLAUDE_SWARM_ROOT_DIR"] = Dir.pwd
562
+ Dir.pwd
565
563
  end
566
564
 
567
- config = Configuration.new(config_file, base_dir: ClaudeSwarm.root_dir)
565
+ config = Configuration.new(config_file, base_dir: root_dir)
568
566
 
569
567
  # Load session metadata if it exists to check for worktree info
570
568
  session_metadata_file = File.join(session_path, "session_metadata.json")
@@ -96,9 +96,8 @@ module ClaudeSwarm
96
96
  main_instance = config.dig("swarm", "main")
97
97
 
98
98
  # Get base directory from session metadata or root_directory file
99
- base_dir = ClaudeSwarm.root_dir
100
99
  root_dir_file = File.join(session_dir, "root_directory")
101
- base_dir = File.read(root_dir_file).strip if File.exist?(root_dir_file)
100
+ base_dir = File.exist?(root_dir_file) ? File.read(root_dir_file).strip : Dir.pwd
102
101
 
103
102
  # Get all directories - handle both string and array formats
104
103
  dir_config = config.dig("swarm", "instances", main_instance, "directory")
@@ -13,13 +13,12 @@ module ClaudeSwarm
13
13
  ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
14
14
  O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
15
15
 
16
- attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :root_directory
16
+ attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :base_dir
17
17
 
18
18
  def initialize(config_path, base_dir: nil, options: {})
19
19
  @config_path = Pathname.new(config_path).expand_path
20
20
  @config_dir = @config_path.dirname
21
- @base_dir = base_dir || @config_dir
22
- @root_directory = @base_dir
21
+ @base_dir = base_dir || @config_dir.to_s
23
22
  @options = options
24
23
  load_and_validate
25
24
  end
@@ -70,19 +69,43 @@ module ClaudeSwarm
70
69
  validate_directories unless has_before_commands?
71
70
  end
72
71
 
73
- def interpolate_env_vars!(obj)
72
+ def interpolate_env_vars!(obj, path = [])
74
73
  case obj
75
74
  when String
76
- interpolate_env_string(obj)
75
+ # Skip interpolation for any values inside MCP configurations
76
+ # Check if we're inside an mcps array element (path like: [..., "instances", <name>, "mcps", <index>, ...])
77
+ if in_mcp_config?(path)
78
+ obj
79
+ else
80
+ interpolate_env_string(obj)
81
+ end
77
82
  when Hash
78
- obj.transform_values! { |v| interpolate_env_vars!(v) }
83
+ obj.each do |key, value|
84
+ obj[key] = interpolate_env_vars!(value, path + [key])
85
+ end
86
+ obj
79
87
  when Array
80
- obj.map! { |v| interpolate_env_vars!(v) }
88
+ obj.map!.with_index { |v, i| interpolate_env_vars!(v, path + [i]) }
81
89
  else
82
90
  obj
83
91
  end
84
92
  end
85
93
 
94
+ def in_mcp_config?(path)
95
+ # Check if we're inside an MCP configuration
96
+ # Pattern: [..., "instances", instance_name, "mcps", index, ...]
97
+ return false if path.size < 4
98
+
99
+ # Find the position of "mcps" in the path
100
+ mcps_index = path.rindex("mcps")
101
+ return false unless mcps_index
102
+
103
+ # Check if this is under instances and followed by an array index
104
+ return false if mcps_index < 2
105
+
106
+ path[mcps_index - 2] == "instances" && path[mcps_index + 1].is_a?(Integer)
107
+ end
108
+
86
109
  def interpolate_env_string(str)
87
110
  str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
88
111
  env_var = Regexp.last_match(1)
@@ -183,21 +183,17 @@ module ClaudeSwarm
183
183
  args.push("--claude-session-id", claude_session_id) if claude_session_id
184
184
  end
185
185
 
186
- # Capture environment variables needed for Ruby and Bundler to work properly
187
- # This includes both BUNDLE_* variables and Ruby-specific variables
186
+ # Capture environment variables needed for running claude-swarm
187
+ # We intentionally exclude Bundler variables to ensure we use the system-installed gem
188
188
  required_env = {}
189
189
 
190
- # Bundle-specific variables
191
- ENV.each do |k, v|
192
- required_env[k] = v if k.start_with?("BUNDLE_")
193
- end
194
-
195
- # Claude Swarm-specific variables
190
+ # Claude Swarm-specific variables (always needed)
196
191
  ENV.each do |k, v|
197
192
  required_env[k] = v if k.start_with?("CLAUDE_SWARM_")
198
193
  end
199
194
 
200
195
  # Ruby-specific variables that MCP servers need
196
+ # Exclude RUBYOPT and RUBYLIB to avoid Bundler interference
201
197
  [
202
198
  "RUBY_ROOT",
203
199
  "RUBY_ENGINE",
@@ -205,8 +201,6 @@ module ClaudeSwarm
205
201
  "GEM_ROOT",
206
202
  "GEM_HOME",
207
203
  "GEM_PATH",
208
- "RUBYOPT",
209
- "RUBYLIB",
210
204
  "PATH",
211
205
  ].each do |key|
212
206
  required_env[key] = ENV[key] if ENV[key]
@@ -38,7 +38,7 @@ module ClaudeSwarm
38
38
  @session_log_path = File.join(@session_path, "session.log")
39
39
  else
40
40
  # Generate new session path
41
- session_params = { working_dir: ClaudeSwarm.root_dir }
41
+ session_params = { working_dir: @config.base_dir }
42
42
  session_params[:session_id] = @provided_session_id if @provided_session_id
43
43
  @session_path = SessionPath.generate(**session_params)
44
44
  SessionPath.ensure_directory(@session_path)
@@ -49,7 +49,6 @@ module ClaudeSwarm
49
49
 
50
50
  end
51
51
  ENV["CLAUDE_SWARM_SESSION_PATH"] = @session_path
52
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
53
52
 
54
53
  # Initialize components that depend on session path
55
54
  @process_tracker = ProcessTracker.new(@session_path)
@@ -235,13 +234,11 @@ module ClaudeSwarm
235
234
  before_commands_dir = parent_dir
236
235
  end
237
236
 
238
- Dir.chdir(before_commands_dir) do
239
- success = execute_before_commands?(before_commands)
240
- unless success
241
- non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
242
- cleanup_all
243
- exit(1)
244
- end
237
+ success = execute_before_commands?(before_commands, chdir: before_commands_dir)
238
+ unless success
239
+ non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
240
+ cleanup_all
241
+ exit(1)
245
242
  end
246
243
 
247
244
  non_interactive_output do
@@ -262,19 +259,18 @@ module ClaudeSwarm
262
259
  end
263
260
 
264
261
  # Execute the main instance - this will cascade to other instances via MCP
265
- Dir.chdir(main_instance[:directory]) do
266
- # Execute main Claude instance with unbundled environment to avoid bundler conflicts
267
- # This ensures the main instance runs in a clean environment without inheriting
268
- # Claude Swarm's BUNDLE_* environment variables
269
- Bundler.with_unbundled_env do
270
- if @non_interactive_prompt
271
- stream_to_session_log(*command)
272
- else
273
- system_with_pid!(*command) do |pid|
274
- @process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
275
- non_interactive_output do
276
- puts "✓ Claude instance started with PID: #{pid}"
277
- end
262
+ # Execute main Claude instance with unbundled environment to avoid bundler conflicts
263
+ # This ensures the main instance runs in a clean environment without inheriting
264
+ # Claude Swarm's BUNDLE_* environment variables
265
+ main_dir = main_instance[:directory]
266
+ Bundler.with_unbundled_env do
267
+ if @non_interactive_prompt
268
+ stream_to_session_log(*command, chdir: main_dir)
269
+ else
270
+ system_with_pid!(*command, chdir: main_dir) do |pid|
271
+ @process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
272
+ non_interactive_output do
273
+ puts "✓ Claude instance started with PID: #{pid}"
278
274
  end
279
275
  end
280
276
  end
@@ -306,12 +302,12 @@ module ClaudeSwarm
306
302
  puts
307
303
  end
308
304
 
309
- def execute_before_commands?(commands)
310
- execute_commands(commands, phase: "before", fail_fast: true)
305
+ def execute_before_commands?(commands, chdir:)
306
+ execute_commands(commands, phase: "before", fail_fast: true, chdir: chdir)
311
307
  end
312
308
 
313
- def execute_after_commands?(commands)
314
- execute_commands(commands, phase: "after", fail_fast: false)
309
+ def execute_after_commands?(commands, chdir:)
310
+ execute_commands(commands, phase: "after", fail_fast: false, chdir: chdir)
315
311
  end
316
312
 
317
313
  def execute_after_commands_once
@@ -336,16 +332,14 @@ module ClaudeSwarm
336
332
  after_commands_dir = parent_dir
337
333
  end
338
334
 
339
- Dir.chdir(after_commands_dir) do
340
- non_interactive_output do
341
- print("⚙️ Executing after commands...")
342
- end
335
+ non_interactive_output do
336
+ print("⚙️ Executing after commands...")
337
+ end
343
338
 
344
- success = execute_after_commands?(after_commands)
345
- unless success
346
- non_interactive_output do
347
- puts "⚠️ Some after commands failed"
348
- end
339
+ success = execute_after_commands?(after_commands, chdir: after_commands_dir)
340
+ unless success
341
+ non_interactive_output do
342
+ puts "⚠️ Some after commands failed"
349
343
  end
350
344
  end
351
345
  end
@@ -357,7 +351,7 @@ module ClaudeSwarm
357
351
 
358
352
  # Save the root directory
359
353
  root_dir_file = File.join(session_path, "root_directory")
360
- File.write(root_dir_file, ClaudeSwarm.root_dir)
354
+ File.write(root_dir_file, @config.base_dir)
361
355
 
362
356
  # Save session metadata
363
357
  metadata_file = File.join(session_path, "session_metadata.json")
@@ -366,7 +360,7 @@ module ClaudeSwarm
366
360
 
367
361
  def build_session_metadata
368
362
  {
369
- "root_directory" => ClaudeSwarm.root_dir,
363
+ "root_directory" => @config.base_dir,
370
364
  "timestamp" => Time.now.utc.iso8601,
371
365
  "start_time" => @start_time.utc.iso8601,
372
366
  "swarm_name" => @config.swarm_name,
@@ -642,12 +636,12 @@ module ClaudeSwarm
642
636
  end
643
637
  end
644
638
 
645
- def stream_to_session_log(*command)
639
+ def stream_to_session_log(*command, chdir:)
646
640
  # Setup logger for session logging
647
641
  logger = Logger.new(@session_log_path, level: :info, progname: @config.main_instance)
648
642
 
649
643
  # Use Open3.popen2e to capture stdout and stderr merged for formatting
650
- Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr|
644
+ Open3.popen2e(*command, chdir: chdir) do |stdin, stdout_and_stderr, wait_thr|
651
645
  stdin.close
652
646
 
653
647
  # Read and process the merged output
@@ -819,7 +813,10 @@ module ClaudeSwarm
819
813
  @logger ||= Logger.new(File.join(@session_path, "session.log"), level: :info, progname: "orchestrator")
820
814
  end
821
815
 
822
- def execute_commands(commands, phase:, fail_fast:)
816
+ def execute_commands(commands, phase:, fail_fast:, chdir:)
817
+ raise ArgumentError, "chdir parameter is required" if chdir.nil?
818
+ raise ArgumentError, "chdir must be a valid directory: #{chdir}" unless File.directory?(chdir)
819
+
823
820
  all_succeeded = true
824
821
 
825
822
  # Setup logger for session logging if we have a session path
@@ -839,13 +836,15 @@ module ClaudeSwarm
839
836
  end
840
837
  end
841
838
 
842
- output = %x(#{command} 2>&1)
843
- success = $CHILD_STATUS.success?
839
+ # Use Open3.capture2e with chdir option to execute the command
840
+ # This allows setting the working directory without changing the process directory
841
+ output, status = Open3.capture2e(command, chdir: chdir)
842
+ success = status.success?
844
843
  output_separator = "-" * 80
845
844
 
846
845
  logger.info { "Command output:" }
847
846
  logger.info { output }
848
- logger.info { "Exit status: #{$CHILD_STATUS.exitstatus}" }
847
+ logger.info { "Exit status: #{status.exitstatus}" }
849
848
  logger.info { output_separator }
850
849
 
851
850
  # Show output if in debug mode or if command failed
@@ -854,7 +853,7 @@ module ClaudeSwarm
854
853
  output_prefix = phase == "after" ? "After command" : "Command"
855
854
  puts "#{output_prefix} #{index + 1} output:"
856
855
  puts output
857
- print("Exit status: #{$CHILD_STATUS.exitstatus}")
856
+ print("Exit status: #{status.exitstatus}")
858
857
  end
859
858
  end
860
859
 
@@ -2,14 +2,14 @@
2
2
 
3
3
  module ClaudeSwarm
4
4
  module SystemUtils
5
- def system!(*args)
6
- system(*args)
5
+ def system!(*args, **options)
6
+ system(*args, **options)
7
7
  handle_command_failure(last_status, args)
8
8
  end
9
9
 
10
- def system_with_pid!(*args)
10
+ def system_with_pid!(*args, **options)
11
11
  # Spawn the process - by default, inherits the parent's I/O
12
- pid = Process.spawn(*args)
12
+ pid = Process.spawn(*args, **options)
13
13
 
14
14
  # Yield the PID to the block if given
15
15
  yield(pid) if block_given?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.6"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -26,25 +26,24 @@ require "fast_mcp"
26
26
  require "mcp_client"
27
27
  require "thor"
28
28
 
29
+ require_relative "claude_swarm/version"
29
30
  # Zeitwerk setup
30
31
  require "zeitwerk"
31
32
  loader = Zeitwerk::Loader.new
32
- loader.tag = "claude_swarm"
33
-
33
+ loader.tag = File.basename(__FILE__, ".rb")
34
34
  loader.ignore("#{__dir__}/claude_swarm/templates")
35
+ loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
36
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
35
37
  loader.inflector.inflect(
36
38
  "cli" => "CLI",
37
39
  "openai" => "OpenAI",
38
40
  )
41
+ loader.setup
39
42
 
40
43
  module ClaudeSwarm
41
44
  class Error < StandardError; end
42
45
 
43
46
  class << self
44
- def root_dir
45
- ENV.fetch("CLAUDE_SWARM_ROOT_DIR") { Dir.pwd }
46
- end
47
-
48
47
  def home_dir
49
48
  ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
50
49
  end
@@ -66,6 +65,3 @@ module ClaudeSwarm
66
65
  end
67
66
  end
68
67
  end
69
-
70
- loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
71
- loader.setup
@@ -29,7 +29,7 @@ module SwarmCLI
29
29
 
30
30
  # Validate the swarm configuration
31
31
  begin
32
- SwarmSDK::Swarm.load(config_path)
32
+ SwarmSDK.load_file(config_path)
33
33
  rescue SwarmSDK::ConfigurationError => e
34
34
  $stderr.puts "Error: Invalid swarm configuration: #{e.message}"
35
35
  exit(1)
@@ -92,7 +92,7 @@ module SwarmCLI
92
92
 
93
93
  define_method(:call) do |task:, description: nil, thinking_budget: nil|
94
94
  # Load swarm for each execution (ensures fresh state)
95
- swarm = SwarmSDK::Swarm.load(self.class.config_path)
95
+ swarm = SwarmSDK.load_file(self.class.config_path)
96
96
 
97
97
  # Build prompt with thinking budget if provided
98
98
  prompt = task
@@ -14,9 +14,9 @@ module SwarmCLI
14
14
 
15
15
  def initialize(options)
16
16
  @options = options
17
- # Create scratchpad with persistence for MCP server
18
- scratchpad_path = File.join(Dir.pwd, ".swarm", "scratchpad.json")
19
- @scratchpad = SwarmSDK::Scratchpad.new(persist_to: scratchpad_path)
17
+ # Create volatile scratchpad for MCP server
18
+ # Note: Scratchpad is always volatile - data is not persisted between sessions
19
+ @scratchpad = SwarmSDK::Tools::Stores::ScratchpadStorage.new
20
20
  end
21
21
 
22
22
  def execute
@@ -4,8 +4,8 @@ module SwarmCLI
4
4
  # ConfigLoader handles loading swarm configurations from both YAML and Ruby DSL files.
5
5
  #
6
6
  # Supports:
7
- # - YAML files (.yml, .yaml) - loaded via SwarmSDK::Swarm.load
8
- # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm instance
7
+ # - YAML files (.yml, .yaml) - loaded via SwarmSDK.load_file
8
+ # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
9
9
  #
10
10
  # @example Load YAML config
11
11
  # swarm = ConfigLoader.load("config.yml")
@@ -18,11 +18,11 @@ module SwarmCLI
18
18
  # Load a swarm configuration from file (YAML or Ruby DSL)
19
19
  #
20
20
  # Detects file type by extension:
21
- # - .yml, .yaml -> Load as YAML using SwarmSDK::Swarm.load
22
- # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm instance
21
+ # - .yml, .yaml -> Load as YAML using SwarmSDK.load_file
22
+ # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
23
23
  #
24
24
  # @param path [String, Pathname] Path to configuration file
25
- # @return [SwarmSDK::Swarm] Configured swarm instance
25
+ # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
26
26
  # @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
27
27
  def load(path)
28
28
  path = Pathname.new(path).expand_path
@@ -50,7 +50,7 @@ module SwarmCLI
50
50
  # @param path [Pathname] Path to YAML file
51
51
  # @return [SwarmSDK::Swarm] Configured swarm instance
52
52
  def load_yaml(path)
53
- SwarmSDK::Swarm.load(path.to_s)
53
+ SwarmSDK.load_file(path.to_s)
54
54
  rescue SwarmSDK::ConfigurationError => e
55
55
  # Re-raise with CLI context
56
56
  raise ConfigurationError, "Configuration error in #{path}: #{e.message}"
@@ -59,12 +59,12 @@ module SwarmCLI
59
59
  # Load Ruby DSL configuration file
60
60
  #
61
61
  # Executes the Ruby file in a clean binding and expects it to return
62
- # a SwarmSDK::Swarm instance. The file should use SwarmSDK.build or
63
- # create a Swarm instance directly.
62
+ # a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. The file should
63
+ # use SwarmSDK.build or create a Swarm/NodeOrchestrator instance directly.
64
64
  #
65
65
  # @param path [Pathname] Path to Ruby DSL file
66
- # @return [SwarmSDK::Swarm] Configured swarm instance
67
- # @raise [ConfigurationError] If file doesn't return a Swarm instance
66
+ # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
67
+ # @raise [ConfigurationError] If file doesn't return a valid instance
68
68
  def load_ruby_dsl(path)
69
69
  # Read the file content
70
70
  content = path.read
@@ -73,10 +73,11 @@ module SwarmCLI
73
73
  # This allows the DSL file to use SwarmSDK.build directly
74
74
  result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
75
75
 
76
- # Validate result is a Swarm instance
77
- unless result.is_a?(SwarmSDK::Swarm)
76
+ # Validate result is a Swarm or NodeOrchestrator instance
77
+ # Both have the same execute(prompt) interface
78
+ unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::NodeOrchestrator)
78
79
  raise ConfigurationError,
79
- "Ruby DSL file must return a SwarmSDK::Swarm instance. " \
80
+ "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. " \
80
81
  "Got: #{result.class}. " \
81
82
  "Use: SwarmSDK.build { ... } or Swarm.new(...)"
82
83
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.1.0"
4
+ VERSION = "2.1.2"
5
5
  end
data/lib/swarm_cli.rb CHANGED
@@ -22,7 +22,9 @@ require_relative "swarm_cli/version"
22
22
 
23
23
  require "zeitwerk"
24
24
  loader = Zeitwerk::Loader.new
25
+ loader.tag = File.basename(__FILE__, ".rb")
25
26
  loader.push_dir("#{__dir__}/swarm_cli", namespace: SwarmCLI)
27
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
26
28
  loader.inflector.inflect(
27
29
  "cli" => "CLI",
28
30
  "ui" => "UI",
@@ -7,11 +7,11 @@ module SwarmMemory
7
7
  # Subclasses must implement all public methods to provide
8
8
  # different storage backends (filesystem, Redis, SQLite, etc.)
9
9
  class Base
10
- # Maximum size per entry (1MB)
11
- MAX_ENTRY_SIZE = 1_000_000
10
+ # Maximum size per entry (3MB)
11
+ MAX_ENTRY_SIZE = 3_000_000
12
12
 
13
- # Maximum total storage size (100MB)
14
- MAX_TOTAL_SIZE = 100_000_000
13
+ # Maximum total storage size (100GB)
14
+ MAX_TOTAL_SIZE = 100_000_000_000
15
15
 
16
16
  # Write content to storage
17
17
  #
@@ -171,12 +171,6 @@ module SwarmMemory
171
171
 
172
172
  content = File.read(md_file)
173
173
 
174
- # Check if it's a stub (redirect)
175
- if stub_content?(content)
176
- target_path = extract_redirect_target(content)
177
- return read(file_path: target_path) if target_path
178
- end
179
-
180
174
  # Increment hit counter
181
175
  increment_hits(file_path)
182
176
 
@@ -205,12 +199,6 @@ module SwarmMemory
205
199
 
206
200
  content = File.read(md_file)
207
201
 
208
- # Follow stub redirect if applicable
209
- if stub_content?(content)
210
- target_path = extract_redirect_target(content)
211
- return read_entry(file_path: target_path) if target_path
212
- end
213
-
214
202
  # Read metadata
215
203
  yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
216
204