swarm_memory 2.0.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.
Files changed (189) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/claude_swarm/base_executor.rb +133 -0
  4. data/lib/claude_swarm/claude_code_executor.rb +349 -0
  5. data/lib/claude_swarm/claude_mcp_server.rb +77 -0
  6. data/lib/claude_swarm/cli.rb +712 -0
  7. data/lib/claude_swarm/commands/ps.rb +216 -0
  8. data/lib/claude_swarm/commands/show.rb +139 -0
  9. data/lib/claude_swarm/configuration.rb +363 -0
  10. data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
  11. data/lib/claude_swarm/json_handler.rb +91 -0
  12. data/lib/claude_swarm/mcp_generator.rb +248 -0
  13. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  14. data/lib/claude_swarm/openai/executor.rb +254 -0
  15. data/lib/claude_swarm/openai/responses.rb +338 -0
  16. data/lib/claude_swarm/orchestrator.rb +879 -0
  17. data/lib/claude_swarm/process_tracker.rb +78 -0
  18. data/lib/claude_swarm/session_cost_calculator.rb +209 -0
  19. data/lib/claude_swarm/session_path.rb +42 -0
  20. data/lib/claude_swarm/settings_generator.rb +77 -0
  21. data/lib/claude_swarm/system_utils.rb +46 -0
  22. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  23. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  24. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/task_tool.rb +63 -0
  26. data/lib/claude_swarm/version.rb +5 -0
  27. data/lib/claude_swarm/worktree_manager.rb +475 -0
  28. data/lib/claude_swarm/yaml_loader.rb +22 -0
  29. data/lib/claude_swarm.rb +69 -0
  30. data/lib/swarm_cli/cli.rb +201 -0
  31. data/lib/swarm_cli/command_registry.rb +61 -0
  32. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  33. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  34. data/lib/swarm_cli/commands/migrate.rb +55 -0
  35. data/lib/swarm_cli/commands/run.rb +173 -0
  36. data/lib/swarm_cli/config_loader.rb +97 -0
  37. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  38. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  39. data/lib/swarm_cli/interactive_repl.rb +918 -0
  40. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  41. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  42. data/lib/swarm_cli/migrate_options.rb +54 -0
  43. data/lib/swarm_cli/migrator.rb +132 -0
  44. data/lib/swarm_cli/options.rb +151 -0
  45. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  46. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  47. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  48. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  49. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  50. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  51. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  52. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  53. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  54. data/lib/swarm_cli/ui/icons.rb +59 -0
  55. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  56. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  57. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  58. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  59. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  60. data/lib/swarm_cli/version.rb +5 -0
  61. data/lib/swarm_cli.rb +45 -0
  62. data/lib/swarm_memory/adapters/base.rb +140 -0
  63. data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
  64. data/lib/swarm_memory/chat_extension.rb +34 -0
  65. data/lib/swarm_memory/cli/commands.rb +306 -0
  66. data/lib/swarm_memory/core/entry.rb +37 -0
  67. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  68. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  69. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  70. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  71. data/lib/swarm_memory/core/storage.rb +286 -0
  72. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  73. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  74. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  75. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  76. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  77. data/lib/swarm_memory/errors.rb +21 -0
  78. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  79. data/lib/swarm_memory/integration/configuration.rb +43 -0
  80. data/lib/swarm_memory/integration/registration.rb +31 -0
  81. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  82. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  83. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  84. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  85. data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
  86. data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
  87. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
  88. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  89. data/lib/swarm_memory/search/text_search.rb +40 -0
  90. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  91. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  92. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  93. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  94. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  95. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  96. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  97. data/lib/swarm_memory/tools/memory_glob.rb +145 -0
  98. data/lib/swarm_memory/tools/memory_grep.rb +209 -0
  99. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  100. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  101. data/lib/swarm_memory/tools/memory_write.rb +215 -0
  102. data/lib/swarm_memory/utils.rb +50 -0
  103. data/lib/swarm_memory/version.rb +5 -0
  104. data/lib/swarm_memory.rb +166 -0
  105. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  106. data/lib/swarm_sdk/agent/builder.rb +461 -0
  107. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  108. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  109. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  110. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  111. data/lib/swarm_sdk/agent/chat.rb +1144 -0
  112. data/lib/swarm_sdk/agent/context.rb +112 -0
  113. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  114. data/lib/swarm_sdk/agent/definition.rb +556 -0
  115. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  116. data/lib/swarm_sdk/configuration.rb +296 -0
  117. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  118. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  119. data/lib/swarm_sdk/context_compactor.rb +340 -0
  120. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  121. data/lib/swarm_sdk/hooks/context.rb +197 -0
  122. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  123. data/lib/swarm_sdk/hooks/error.rb +29 -0
  124. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  125. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  126. data/lib/swarm_sdk/hooks/result.rb +150 -0
  127. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  128. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  129. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  130. data/lib/swarm_sdk/log_collector.rb +51 -0
  131. data/lib/swarm_sdk/log_stream.rb +69 -0
  132. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  133. data/lib/swarm_sdk/model_aliases.json +5 -0
  134. data/lib/swarm_sdk/models.json +1 -0
  135. data/lib/swarm_sdk/models.rb +120 -0
  136. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  137. data/lib/swarm_sdk/node/builder.rb +439 -0
  138. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  139. data/lib/swarm_sdk/node_context.rb +170 -0
  140. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  141. data/lib/swarm_sdk/permissions/config.rb +239 -0
  142. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  143. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  144. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  145. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  146. data/lib/swarm_sdk/plugin.rb +147 -0
  147. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  148. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  149. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  150. data/lib/swarm_sdk/result.rb +97 -0
  151. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  152. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  153. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  154. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  155. data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
  156. data/lib/swarm_sdk/swarm.rb +982 -0
  157. data/lib/swarm_sdk/tools/bash.rb +274 -0
  158. data/lib/swarm_sdk/tools/clock.rb +44 -0
  159. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  160. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  161. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  162. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  163. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  164. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  165. data/lib/swarm_sdk/tools/edit.rb +150 -0
  166. data/lib/swarm_sdk/tools/glob.rb +158 -0
  167. data/lib/swarm_sdk/tools/grep.rb +228 -0
  168. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  169. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  170. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  171. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  172. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  173. data/lib/swarm_sdk/tools/read.rb +251 -0
  174. data/lib/swarm_sdk/tools/registry.rb +93 -0
  175. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  176. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  177. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  178. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  179. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  180. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  181. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  182. data/lib/swarm_sdk/tools/think.rb +95 -0
  183. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  184. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  185. data/lib/swarm_sdk/tools/write.rb +117 -0
  186. data/lib/swarm_sdk/utils.rb +50 -0
  187. data/lib/swarm_sdk/version.rb +5 -0
  188. data/lib/swarm_sdk.rb +167 -0
  189. metadata +313 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9cc16f06662bd282f8c296aeb6600f53dd80d14f27afad26b9da6f724916d20a
4
+ data.tar.gz: 6acf6425bdc544f125826ac5ba1f6b127f9c3d96e7ba067712cd15f165ac5af5
5
+ SHA512:
6
+ metadata.gz: 91b645763ed45723d82080fb2321ca5e9c0d61310c3f3907843c9086980f5b27e0176315b2ac399f837191c5559b6e36563e2f71bb18adaf2a93a957f186a2e9
7
+ data.tar.gz: 9b019e85c71c3f7c9f1c1ac5358c53566b5c5934fb1e983087e9c6ebdd81c79b1e32c100d762161bda4aff2227c2be24bfa3d9f0784edc7cbd227a9a21d724af
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Paulo Arruda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class BaseExecutor
5
+ attr_reader :session_id, :last_response, :working_directory, :logger, :session_path, :session_json_path, :instance_info
6
+
7
+ def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
8
+ instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
9
+ claude_session_id: nil, additional_directories: [], debug: false)
10
+ @working_directory = working_directory
11
+ @additional_directories = additional_directories
12
+ @model = model
13
+ @mcp_config = mcp_config
14
+ @vibe = vibe
15
+ @session_id = claude_session_id
16
+ @last_response = nil
17
+ @instance_name = instance_name
18
+ @instance_id = instance_id
19
+ @calling_instance = calling_instance
20
+ @calling_instance_id = calling_instance_id
21
+ @debug = debug
22
+
23
+ # Setup static info strings for logging
24
+ @instance_info = build_info(@instance_name, @instance_id)
25
+ @caller_info = build_info(@calling_instance, @calling_instance_id)
26
+ @caller_to_instance = "#{@caller_info} -> #{instance_info}:"
27
+ @instance_to_caller = "#{instance_info} -> #{@caller_info}:"
28
+
29
+ # Setup logging
30
+ setup_logging
31
+
32
+ # Setup static event templates
33
+ setup_event_templates
34
+ end
35
+
36
+ def execute(_prompt, _options = {})
37
+ raise NotImplementedError, "Subclasses must implement the execute method"
38
+ end
39
+
40
+ def reset_session
41
+ @session_id = nil
42
+ @last_response = nil
43
+ end
44
+
45
+ def has_session?
46
+ !@session_id.nil?
47
+ end
48
+
49
+ protected
50
+
51
+ def build_info(name, id)
52
+ return name unless id
53
+
54
+ "#{name} (#{id})"
55
+ end
56
+
57
+ def setup_logging
58
+ # Use session path from environment (required)
59
+ @session_path = SessionPath.from_env
60
+ SessionPath.ensure_directory(@session_path)
61
+
62
+ # Initialize session JSON path
63
+ @session_json_path = File.join(@session_path, "session.log.json")
64
+
65
+ # Create logger with session.log filename
66
+ log_filename = "session.log"
67
+ log_path = File.join(@session_path, log_filename)
68
+ log_level = @debug ? :debug : :info
69
+ @logger = Logger.new(log_path, level: log_level, progname: @instance_name)
70
+
71
+ logger.info { "Started #{self.class.name} for instance: #{instance_info}" }
72
+ end
73
+
74
+ def setup_event_templates
75
+ @log_request_event_template = {
76
+ type: "request",
77
+ from_instance: @calling_instance,
78
+ from_instance_id: @calling_instance_id,
79
+ to_instance: @instance_name,
80
+ to_instance_id: @instance_id,
81
+ }.freeze
82
+
83
+ @session_json_entry_template = {
84
+ instance: @instance_name,
85
+ instance_id: @instance_id,
86
+ calling_instance: @calling_instance,
87
+ calling_instance_id: @calling_instance_id,
88
+ }.freeze
89
+ end
90
+
91
+ def log_request(prompt)
92
+ logger.info { "#{@caller_to_instance} \n---\n#{prompt}\n---" }
93
+
94
+ # Merge dynamic data with static template
95
+ event = @log_request_event_template.merge(
96
+ prompt: prompt,
97
+ timestamp: Time.now.iso8601,
98
+ )
99
+
100
+ append_to_session_json(event)
101
+ end
102
+
103
+ def log_response(response)
104
+ logger.info do
105
+ "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{@instance_to_caller} \n---\n#{response["result"]}\n---"
106
+ end
107
+ end
108
+
109
+ def append_to_session_json(event)
110
+ # Use file locking to ensure thread-safe writes
111
+ File.open(@session_json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
112
+ file.flock(File::LOCK_EX)
113
+
114
+ # Merge dynamic data with static template
115
+ entry = @session_json_entry_template.merge(
116
+ timestamp: Time.now.iso8601,
117
+ event: event,
118
+ )
119
+
120
+ # Write as single line JSON (JSONL format)
121
+ file.puts(entry.to_json)
122
+
123
+ file.flock(File::LOCK_UN)
124
+ end
125
+ rescue StandardError => e
126
+ logger.error { "Failed to append to session JSON: #{e.message}" }
127
+ raise
128
+ end
129
+
130
+ class ExecutionError < StandardError; end
131
+ class ParseError < StandardError; end
132
+ end
133
+ end
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class ClaudeCodeExecutor < BaseExecutor
5
+ def execute(prompt, options = {})
6
+ # Log the request
7
+ log_request(prompt)
8
+
9
+ # Build SDK options
10
+ sdk_options = build_sdk_options(prompt, options)
11
+
12
+ # Variables to collect output
13
+ all_messages = []
14
+ result_response = nil
15
+
16
+ # Execute with streaming
17
+ begin
18
+ ClaudeSDK.query(prompt, options: sdk_options) do |message|
19
+ # Convert message to hash for logging
20
+ message_hash = message_to_hash(message)
21
+ all_messages << message_hash
22
+
23
+ # Log streaming event BEFORE we modify anything
24
+ log_streaming_event(message_hash)
25
+
26
+ # Process specific message types
27
+ case message
28
+ when ClaudeSDK::Messages::System
29
+ # Capture session_id from system init
30
+ if message.subtype == "init" && message.data.is_a?(Hash)
31
+ # For init messages, session_id is in the data hash
32
+ session_id = message.data[:session_id] || message.data["session_id"]
33
+
34
+ if session_id
35
+ @session_id = session_id
36
+ write_instance_state
37
+ end
38
+ end
39
+ when ClaudeSDK::Messages::Assistant
40
+ # Assistant messages only contain content blocks
41
+ # No need to track for result extraction - result comes from Result message
42
+ when ClaudeSDK::Messages::Result
43
+ # Validate that we have actual result content
44
+ if message.result.nil? || (message.result.is_a?(String) && message.result.strip.empty?)
45
+ raise ExecutionError, "Claude SDK returned an empty result. The agent completed execution but provided no response content."
46
+ end
47
+
48
+ # Build result response in expected format
49
+ result_response = {
50
+ "type" => "result",
51
+ "subtype" => message.subtype || "success",
52
+ "cost_usd" => message.total_cost_usd,
53
+ "is_error" => message.is_error || false,
54
+ "duration_ms" => message.duration_ms,
55
+ "result" => message.result, # Result text is directly in message.result
56
+ "total_cost" => message.total_cost_usd,
57
+ "session_id" => message.session_id,
58
+ }
59
+ end
60
+ end
61
+ rescue StandardError => e
62
+ logger.error { "Execution error for #{@instance_name}: #{e.class} - #{e.message}" }
63
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
64
+ raise ExecutionError, "Claude Code execution failed: #{e.message}"
65
+ end
66
+
67
+ # Ensure we got a result
68
+ raise ParseError, "No result found in SDK response" unless result_response
69
+
70
+ # Write session JSON log
71
+ all_messages.each do |msg|
72
+ append_to_session_json(msg)
73
+ end
74
+
75
+ result_response
76
+ rescue StandardError => e
77
+ logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
78
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
79
+ raise
80
+ end
81
+
82
+ private
83
+
84
+ def write_instance_state
85
+ return unless @instance_id && @session_id
86
+
87
+ state_dir = File.join(@session_path, "state")
88
+ FileUtils.mkdir_p(state_dir)
89
+
90
+ state_file = File.join(state_dir, "#{@instance_id}.json")
91
+ state_data = {
92
+ instance_name: @instance_name,
93
+ instance_id: @instance_id,
94
+ claude_session_id: @session_id,
95
+ status: "active",
96
+ updated_at: Time.now.iso8601,
97
+ }
98
+
99
+ JsonHandler.write_file!(state_file, state_data)
100
+ logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
101
+ rescue StandardError => e
102
+ logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
103
+ end
104
+
105
+ def log_streaming_event(event)
106
+ append_to_session_json(event)
107
+
108
+ return log_system_message(event) if event["type"] == "system"
109
+
110
+ # Add specific details based on event type
111
+ case event["type"]
112
+ when "assistant"
113
+ log_assistant_message(event["message"])
114
+ when "user"
115
+ log_user_message(event["message"]["content"])
116
+ when "result"
117
+ log_response(event)
118
+ end
119
+ end
120
+
121
+ def log_system_message(event)
122
+ logger.debug { "SYSTEM: #{JsonHandler.pretty_generate!(event)}" }
123
+ end
124
+
125
+ def log_assistant_message(msg)
126
+ # Assistant messages don't have stop_reason in SDK - they only have content
127
+ content = msg["content"]
128
+ logger.debug { "ASSISTANT: #{JsonHandler.pretty_generate!(content)}" } if content
129
+
130
+ # Log tool calls
131
+ tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
132
+ tool_calls.each do |tool_call|
133
+ arguments = tool_call["input"].to_json
134
+ arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
135
+
136
+ logger.info do
137
+ "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
138
+ end
139
+ end
140
+
141
+ # Log thinking text
142
+ text = content&.select { |c| c["type"] == "text" } || []
143
+ text.each do |t|
144
+ logger.info { "#{instance_info} is thinking:\n---\n#{t["text"]}\n---" }
145
+ end
146
+ end
147
+
148
+ def log_user_message(content)
149
+ logger.debug { "USER: #{JsonHandler.pretty_generate!(content)}" }
150
+ end
151
+
152
+ def build_sdk_options(prompt, options)
153
+ # Map CLI options to SDK options
154
+ sdk_options = ClaudeSDK::ClaudeCodeOptions.new
155
+
156
+ # Basic options
157
+ # Only set model if ANTHROPIC_MODEL env var is not set
158
+ sdk_options.model = @model if @model && !ENV["ANTHROPIC_MODEL"]
159
+ sdk_options.cwd = @working_directory
160
+ sdk_options.resume = @session_id if @session_id && !options[:new_session]
161
+
162
+ # Permission mode
163
+ if @vibe
164
+ sdk_options.permission_mode = ClaudeSDK::PermissionMode::BYPASS_PERMISSIONS
165
+ else
166
+ # Build allowed tools list including MCP connections
167
+ allowed_tools = options[:allowed_tools] ? Array(options[:allowed_tools]).dup : []
168
+
169
+ # Add mcp__instance_name for each connection if we have instance info
170
+ options[:connections]&.each do |connection_name|
171
+ allowed_tools << "mcp__#{connection_name}"
172
+ end
173
+
174
+ # Set allowed and disallowed tools
175
+ sdk_options.allowed_tools = allowed_tools if allowed_tools.any?
176
+ sdk_options.disallowed_tools = Array(options[:disallowed_tools]) if options[:disallowed_tools]
177
+ end
178
+
179
+ # System prompt
180
+ sdk_options.append_system_prompt = options[:system_prompt] if options[:system_prompt]
181
+
182
+ # MCP configuration
183
+ if @mcp_config
184
+ sdk_options.mcp_servers = parse_mcp_config(@mcp_config)
185
+ end
186
+
187
+ # Handle additional directories by adding them to MCP servers
188
+ if @additional_directories.any?
189
+ setup_additional_directories_mcp(sdk_options)
190
+ end
191
+
192
+ # Add settings file path if it exists
193
+ settings_file = File.join(@session_path, "#{@instance_name}_settings.json")
194
+ sdk_options.settings = settings_file if File.exist?(settings_file)
195
+
196
+ sdk_options
197
+ end
198
+
199
+ def parse_mcp_config(config_path)
200
+ # Parse MCP JSON config file and convert to SDK format
201
+ config = JsonHandler.parse_file!(config_path)
202
+ mcp_servers = {}
203
+
204
+ config["mcpServers"]&.each do |name, server_config|
205
+ server_type = server_config["type"] || "stdio"
206
+
207
+ mcp_servers[name] = case server_type
208
+ when "stdio"
209
+ ClaudeSDK::McpServerConfig::StdioServer.new(
210
+ command: server_config["command"],
211
+ args: server_config["args"] || [],
212
+ env: server_config["env"] || {},
213
+ )
214
+ when "sse"
215
+ ClaudeSDK::McpServerConfig::SSEServer.new(
216
+ url: server_config["url"],
217
+ headers: server_config["headers"] || {},
218
+ )
219
+ when "http"
220
+ ClaudeSDK::McpServerConfig::HttpServer.new(
221
+ url: server_config["url"],
222
+ headers: server_config["headers"] || {},
223
+ )
224
+ else
225
+ logger.warn { "Unsupported MCP server type: #{server_type} for server: #{name}" }
226
+ nil
227
+ end
228
+ end
229
+
230
+ mcp_servers.compact
231
+ rescue StandardError => e
232
+ logger.error { "Failed to parse MCP config: #{e.message}" }
233
+ {}
234
+ end
235
+
236
+ def setup_additional_directories_mcp(sdk_options)
237
+ # Workaround for --add-dir: add file system MCP servers for additional directories
238
+ sdk_options.mcp_servers ||= {}
239
+
240
+ @additional_directories.each do |dir|
241
+ # This is a placeholder - the SDK doesn't directly support file system servers
242
+ # You would need to implement a proper MCP server that provides file access
243
+ logger.warn { "Additional directories not fully supported: #{dir}" }
244
+ end
245
+ end
246
+
247
+ def message_to_hash(message)
248
+ # Convert SDK message objects to hash format matching CLI JSON output
249
+ case message
250
+ when ClaudeSDK::Messages::System
251
+ # System messages have subtype and data attributes
252
+ # The data hash contains the actual information from the CLI
253
+ hash = {
254
+ "type" => "system",
255
+ "subtype" => message.subtype,
256
+ }
257
+
258
+ # Include the data hash if it exists - this is where CLI puts info like session_id, tools, etc.
259
+ if message.data.is_a?(Hash)
260
+ # For "init" subtype, extract session_id and tools from data
261
+ if message.subtype == "init"
262
+ hash["session_id"] = message.data[:session_id] || message.data["session_id"]
263
+ hash["tools"] = message.data[:tools] || message.data["tools"]
264
+ end
265
+ # You can add other relevant data fields as needed
266
+ end
267
+
268
+ hash.compact
269
+ when ClaudeSDK::Messages::Assistant
270
+ # Assistant messages only have content attribute
271
+ {
272
+ "type" => "assistant",
273
+ "message" => {
274
+ "type" => "message",
275
+ "role" => "assistant",
276
+ "content" => content_blocks_to_hash(message.content),
277
+ },
278
+ "session_id" => @session_id,
279
+ }
280
+ when ClaudeSDK::Messages::User
281
+ # User messages only have content attribute (a string)
282
+ {
283
+ "type" => "user",
284
+ "message" => {
285
+ "type" => "message",
286
+ "role" => "user",
287
+ "content" => message.content,
288
+ },
289
+ "session_id" => @session_id,
290
+ }
291
+ when ClaudeSDK::Messages::Result
292
+ # Result messages have multiple attributes
293
+ {
294
+ "type" => "result",
295
+ "subtype" => message.subtype || "success",
296
+ "cost_usd" => message.total_cost_usd,
297
+ "is_error" => message.is_error || false,
298
+ "duration_ms" => message.duration_ms,
299
+ "duration_api_ms" => message.duration_api_ms,
300
+ "num_turns" => message.num_turns,
301
+ "result" => message.result, # Result text is in message.result, not from content
302
+ "total_cost" => message.total_cost_usd,
303
+ "total_cost_usd" => message.total_cost_usd,
304
+ "session_id" => message.session_id,
305
+ "usage" => message.usage,
306
+ }.compact
307
+ else
308
+ # Fallback for unknown message types
309
+ begin
310
+ message.to_h
311
+ rescue
312
+ { "type" => "unknown", "data" => message.to_s }
313
+ end
314
+ end
315
+ end
316
+
317
+ def content_blocks_to_hash(content)
318
+ return [] unless content
319
+
320
+ content.map do |block|
321
+ case block
322
+ when ClaudeSDK::ContentBlock::Text
323
+ { "type" => "text", "text" => block.text }
324
+ when ClaudeSDK::ContentBlock::ToolUse
325
+ {
326
+ "type" => "tool_use",
327
+ "id" => block.id,
328
+ "name" => block.name,
329
+ "input" => block.input,
330
+ }
331
+ when ClaudeSDK::ContentBlock::ToolResult
332
+ {
333
+ "type" => "tool_result",
334
+ "tool_use_id" => block.tool_use_id,
335
+ "content" => block.content,
336
+ "is_error" => block.is_error,
337
+ }
338
+ else
339
+ # Fallback
340
+ begin
341
+ block.to_h
342
+ rescue
343
+ { "type" => "unknown", "data" => block.to_s }
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class ClaudeMcpServer
5
+ # Class variables to share state with tool classes
6
+ class << self
7
+ attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
8
+ end
9
+
10
+ def initialize(instance_config, calling_instance:, calling_instance_id: nil, debug: false)
11
+ @instance_config = instance_config
12
+ @calling_instance = calling_instance
13
+ @calling_instance_id = calling_instance_id
14
+
15
+ # Create appropriate executor based on provider
16
+ common_params = {
17
+ working_directory: instance_config[:directory],
18
+ model: instance_config[:model],
19
+ mcp_config: instance_config[:mcp_config_path],
20
+ vibe: instance_config[:vibe],
21
+ instance_name: instance_config[:name],
22
+ instance_id: instance_config[:instance_id],
23
+ calling_instance: calling_instance,
24
+ calling_instance_id: calling_instance_id,
25
+ claude_session_id: instance_config[:claude_session_id],
26
+ additional_directories: instance_config[:directories][1..] || [],
27
+ debug: debug,
28
+ }
29
+
30
+ @executor = if instance_config[:provider] == "openai"
31
+ OpenAI::Executor.new(
32
+ **common_params,
33
+ # OpenAI-specific parameters
34
+ temperature: instance_config[:temperature],
35
+ api_version: instance_config[:api_version],
36
+ openai_token_env: instance_config[:openai_token_env],
37
+ base_url: instance_config[:base_url],
38
+ reasoning_effort: instance_config[:reasoning_effort],
39
+ )
40
+ else
41
+ # Default Claude behavior - always use SDK
42
+ ClaudeCodeExecutor.new(**common_params)
43
+ end
44
+
45
+ # Set class variables so tools can access them
46
+ self.class.executor = @executor
47
+ self.class.instance_config = @instance_config
48
+ self.class.logger = @executor.logger
49
+ self.class.session_path = @executor.session_path
50
+ self.class.calling_instance = @calling_instance
51
+ self.class.calling_instance_id = @calling_instance_id
52
+ end
53
+
54
+ def start
55
+ server = FastMcp::Server.new(
56
+ name: @instance_config[:name],
57
+ version: "1.0.0",
58
+ )
59
+
60
+ # Set dynamic description for TaskTool based on instance config
61
+ thinking_info = " Thinking budget levels: \"think\" < \"think hard\" < \"think harder\" < \"ultrathink\"."
62
+ if @instance_config[:description]
63
+ Tools::TaskTool.description("Execute a task using Agent #{@instance_config[:name]}. #{@instance_config[:description]} #{thinking_info}")
64
+ else
65
+ Tools::TaskTool.description("Execute a task using Agent #{@instance_config[:name]}. #{thinking_info}")
66
+ end
67
+
68
+ # Register tool classes (not instances)
69
+ server.register_tool(Tools::TaskTool)
70
+ server.register_tool(Tools::SessionInfoTool)
71
+ server.register_tool(Tools::ResetSessionTool)
72
+
73
+ # Start the stdio server
74
+ server.start
75
+ end
76
+ end
77
+ end