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
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class Configuration
5
+ # Frozen constants for validation
6
+ VALID_PROVIDERS = ["claude", "openai"].freeze
7
+ OPENAI_SPECIFIC_FIELDS = ["temperature", "api_version", "openai_token_env", "base_url", "reasoning_effort"].freeze
8
+ VALID_API_VERSIONS = ["chat_completion", "responses"].freeze
9
+ VALID_REASONING_EFFORTS = ["low", "medium", "high"].freeze
10
+
11
+ # Regex patterns
12
+ ENV_VAR_PATTERN = /\$\{([^}]+)\}/
13
+ ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
14
+ O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
15
+
16
+ attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :root_directory
17
+
18
+ def initialize(config_path, base_dir: nil, options: {})
19
+ @config_path = Pathname.new(config_path).expand_path
20
+ @config_dir = @config_path.dirname
21
+ @base_dir = base_dir || @config_dir
22
+ @root_directory = @base_dir
23
+ @options = options
24
+ load_and_validate
25
+ end
26
+
27
+ def main_instance_config
28
+ instances[main_instance]
29
+ end
30
+
31
+ def instance_names
32
+ instances.keys
33
+ end
34
+
35
+ def connections_for(instance_name)
36
+ instances[instance_name][:connections] || []
37
+ end
38
+
39
+ def before_commands
40
+ @swarm["before"] || []
41
+ end
42
+
43
+ def after_commands
44
+ @swarm["after"] || []
45
+ end
46
+
47
+ def validate_directories
48
+ @instances.each do |name, instance|
49
+ # Validate all directories in the directories array
50
+ instance[:directories].each do |directory|
51
+ raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def has_before_commands?
59
+ @swarm && @swarm["before"] && !@swarm["before"].empty?
60
+ end
61
+
62
+ def load_and_validate
63
+ @config = YamlLoader.load_config_file(@config_path)
64
+ interpolate_env_vars!(@config)
65
+ validate_version
66
+ validate_swarm
67
+ parse_swarm
68
+ # Skip directory validation if before commands are present
69
+ # They might create the directories
70
+ validate_directories unless has_before_commands?
71
+ end
72
+
73
+ def interpolate_env_vars!(obj)
74
+ case obj
75
+ when String
76
+ interpolate_env_string(obj)
77
+ when Hash
78
+ obj.transform_values! { |v| interpolate_env_vars!(v) }
79
+ when Array
80
+ obj.map! { |v| interpolate_env_vars!(v) }
81
+ else
82
+ obj
83
+ end
84
+ end
85
+
86
+ def interpolate_env_string(str)
87
+ str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
88
+ env_var = Regexp.last_match(1)
89
+ has_default = Regexp.last_match(2)
90
+ default_value = Regexp.last_match(3)
91
+
92
+ if ENV.key?(env_var)
93
+ ENV[env_var]
94
+ elsif has_default
95
+ default_value || ""
96
+ else
97
+ raise Error, "Environment variable '#{env_var}' is not set"
98
+ end
99
+ end
100
+ end
101
+
102
+ def validate_version
103
+ version = @config["version"]
104
+ raise Error, "Missing 'version' field in configuration" unless version
105
+ raise Error, "Unsupported version: #{version}. Only version 1 is supported" unless version == 1
106
+ end
107
+
108
+ def validate_swarm
109
+ raise Error, "Missing 'swarm' field in configuration" unless @config["swarm"]
110
+
111
+ swarm = @config["swarm"]
112
+ raise Error, "Missing 'name' field in swarm configuration" unless swarm["name"]
113
+ raise Error, "Missing 'instances' field in swarm configuration" unless swarm["instances"]
114
+ raise Error, "Missing 'main' field in swarm configuration" unless swarm["main"]
115
+
116
+ raise Error, "No instances defined" if swarm["instances"].empty?
117
+
118
+ main = swarm["main"]
119
+ raise Error, "Main instance '#{main}' not found in instances" unless swarm["instances"].key?(main)
120
+ end
121
+
122
+ def parse_swarm
123
+ @swarm = @config["swarm"]
124
+ @swarm_name = @swarm["name"]
125
+ @main_instance = @swarm["main"]
126
+ @instances = {}
127
+ @swarm["instances"].each do |name, config|
128
+ @instances[name] = parse_instance(name, config)
129
+ end
130
+ validate_main_instance_provider
131
+ validate_connections
132
+ detect_circular_dependencies
133
+ validate_openai_env_vars
134
+ validate_openai_responses_api_compatibility
135
+ end
136
+
137
+ def parse_instance(name, config)
138
+ config ||= {}
139
+
140
+ # Validate required fields
141
+ raise Error, "Instance '#{name}' missing required 'description' field" unless config["description"]
142
+
143
+ # Parse provider (optional, defaults to claude)
144
+ provider = config["provider"]
145
+ model = config["model"]
146
+
147
+ # Validate provider value if specified
148
+ if provider && !VALID_PROVIDERS.include?(provider)
149
+ raise Error, "Instance '#{name}' has invalid provider '#{provider}'. Must be 'claude' or 'openai'"
150
+ end
151
+
152
+ # Validate reasoning_effort for OpenAI provider
153
+ if config["reasoning_effort"]
154
+ # Ensure it's only used with OpenAI provider
155
+ if provider != "openai"
156
+ raise Error, "Instance '#{name}' has reasoning_effort but provider is not 'openai'"
157
+ end
158
+
159
+ # Validate the value
160
+ unless VALID_REASONING_EFFORTS.include?(config["reasoning_effort"])
161
+ raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
162
+ end
163
+
164
+ # Validate it's only used with o-series or gpt-5 models
165
+ # Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.
166
+ unless model&.match?(O_SERIES_MODEL_PATTERN)
167
+ raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series or gpt-5 model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.)"
168
+ end
169
+ end
170
+
171
+ # Validate temperature is not used with o-series or gpt-5 models when provider is openai
172
+ if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
173
+ raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series or gpt-5 model. O-series and gpt-5 models use deterministic reasoning and don't accept temperature settings"
174
+ end
175
+
176
+ # Validate OpenAI-specific fields only when provider is not "openai"
177
+ if provider != "openai"
178
+ invalid_fields = OPENAI_SPECIFIC_FIELDS & config.keys
179
+ unless invalid_fields.empty?
180
+ raise Error, "Instance '#{name}' has OpenAI-specific fields #{invalid_fields.join(", ")} but provider is not 'openai'"
181
+ end
182
+ end
183
+
184
+ # Validate api_version if specified
185
+ if config["api_version"] && !VALID_API_VERSIONS.include?(config["api_version"])
186
+ raise Error, "Instance '#{name}' has invalid api_version '#{config["api_version"]}'. Must be 'chat_completion' or 'responses'"
187
+ end
188
+
189
+ # Validate tool fields are arrays if present
190
+ validate_tool_field(name, config, "tools")
191
+ validate_tool_field(name, config, "allowed_tools")
192
+ validate_tool_field(name, config, "disallowed_tools")
193
+
194
+ # Support both 'tools' (deprecated) and 'allowed_tools' for backward compatibility
195
+ allowed_tools = config["allowed_tools"] || config["tools"] || []
196
+
197
+ # Parse directory field - support both string and array
198
+ directories = parse_directories(config["directory"])
199
+
200
+ instance_config = {
201
+ name: name,
202
+ directory: directories.first, # Keep single directory for backward compatibility
203
+ directories: directories, # New field with all directories
204
+ model: config["model"] || "sonnet",
205
+ connections: Array(config["connections"]),
206
+ tools: Array(allowed_tools), # Keep as 'tools' internally for compatibility
207
+ allowed_tools: Array(allowed_tools),
208
+ disallowed_tools: Array(config["disallowed_tools"]),
209
+ mcps: parse_mcps(config["mcps"] || []),
210
+ prompt: config["prompt"],
211
+ prompt_file: config["prompt_file"],
212
+ description: config["description"],
213
+ vibe: config["vibe"],
214
+ worktree: parse_worktree_value(config["worktree"]),
215
+ provider: provider, # nil means Claude (default)
216
+ hooks: config["hooks"], # Pass hooks configuration as-is
217
+ }
218
+
219
+ # Add OpenAI-specific fields only when provider is "openai"
220
+ if provider == "openai"
221
+ instance_config[:temperature] = config["temperature"] if config["temperature"]
222
+ instance_config[:api_version] = config["api_version"] || "chat_completion"
223
+ instance_config[:openai_token_env] = config["openai_token_env"] || "OPENAI_API_KEY"
224
+ instance_config[:base_url] = config["base_url"]
225
+ instance_config[:reasoning_effort] = config["reasoning_effort"] if config["reasoning_effort"]
226
+ # Default vibe to true for OpenAI instances if not specified
227
+ instance_config[:vibe] = true if config["vibe"].nil?
228
+ elsif config["vibe"].nil?
229
+ # Default vibe to false for Claude instances if not specified
230
+ instance_config[:vibe] = false
231
+ end
232
+
233
+ instance_config
234
+ end
235
+
236
+ def parse_mcps(mcps)
237
+ mcps.map do |mcp|
238
+ validate_mcp(mcp)
239
+ mcp
240
+ end
241
+ end
242
+
243
+ def validate_mcp(mcp)
244
+ raise Error, "MCP configuration missing 'name'" unless mcp["name"]
245
+
246
+ case mcp["type"]
247
+ when "stdio"
248
+ raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
249
+ when "sse", "http"
250
+ raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
251
+ else
252
+ raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
253
+ end
254
+ end
255
+
256
+ def validate_connections
257
+ @instances.each do |name, instance|
258
+ instance[:connections].each do |connection|
259
+ raise Error, "Instance '#{name}' has connection to unknown instance '#{connection}'" unless @instances.key?(connection)
260
+ end
261
+ end
262
+ end
263
+
264
+ def detect_circular_dependencies
265
+ @instances.each_key do |instance_name|
266
+ visited = Set.new
267
+ path = []
268
+ detect_cycle_from(instance_name, visited, path)
269
+ end
270
+ end
271
+
272
+ def detect_cycle_from(instance_name, visited, path)
273
+ return if visited.include?(instance_name)
274
+
275
+ if path.include?(instance_name)
276
+ cycle_start = path.index(instance_name)
277
+ cycle = path[cycle_start..] + [instance_name]
278
+ raise Error, "Circular dependency detected: #{cycle.join(" -> ")}"
279
+ end
280
+
281
+ path.push(instance_name)
282
+ @instances[instance_name][:connections].each do |connection|
283
+ detect_cycle_from(connection, visited, path)
284
+ end
285
+ path.pop
286
+ visited.add(instance_name)
287
+ end
288
+
289
+ def validate_tool_field(instance_name, config, field_name)
290
+ return unless config.key?(field_name)
291
+
292
+ field_value = config[field_name]
293
+ raise Error, "Instance '#{instance_name}' field '#{field_name}' must be an array, got #{field_value.class.name}" unless field_value.is_a?(Array)
294
+ end
295
+
296
+ def parse_directories(directory_config)
297
+ # Default to current directory if not specified
298
+ directory_config ||= "."
299
+
300
+ # Convert to array and expand paths
301
+ directories = Array(directory_config).map { |dir| expand_path(dir) }
302
+
303
+ # Ensure at least one directory
304
+ directories.empty? ? [expand_path(".")] : directories
305
+ end
306
+
307
+ def expand_path(path)
308
+ Pathname.new(path).expand_path(@base_dir).to_s
309
+ end
310
+
311
+ def parse_worktree_value(value)
312
+ return if value.nil? # Omitted means follow CLI behavior
313
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
314
+ return value.to_s if value.is_a?(String) && !value.empty?
315
+
316
+ raise Error, "Invalid worktree value: #{value.inspect}. Must be true, false, or a non-empty string"
317
+ end
318
+
319
+ def validate_openai_env_vars
320
+ @instances.each_value do |instance|
321
+ next unless instance[:provider] == "openai"
322
+
323
+ env_var = instance[:openai_token_env]
324
+ unless ENV.key?(env_var) && !ENV[env_var].to_s.strip.empty?
325
+ raise Error, "Environment variable '#{env_var}' is not set. OpenAI provider instances require an API key."
326
+ end
327
+ end
328
+ end
329
+
330
+ def validate_main_instance_provider
331
+ # Only validate in interactive mode (when no prompt is provided)
332
+ return if @options[:prompt]
333
+
334
+ main_config = @instances[@main_instance]
335
+ if main_config[:provider]
336
+ raise Error, "Main instance '#{@main_instance}' cannot have a provider setting in interactive mode"
337
+ end
338
+ end
339
+
340
+ def validate_openai_responses_api_compatibility
341
+ # Check if any instance uses OpenAI provider with responses API
342
+ responses_api_instances = @instances.select do |_name, instance|
343
+ instance[:provider] == "openai" && instance[:api_version] == "responses"
344
+ end
345
+
346
+ return if responses_api_instances.empty?
347
+
348
+ # Check ruby-openai version
349
+ begin
350
+ require "openai/version"
351
+ openai_version = Gem::Version.new(::OpenAI::VERSION)
352
+ required_version = Gem::Version.new("8.0.0")
353
+
354
+ if openai_version < required_version
355
+ instance_names = responses_api_instances.keys.join(", ")
356
+ raise Error, "Instances #{instance_names} use OpenAI provider with api_version 'responses', which requires ruby-openai >= 8.0. Current version is #{openai_version}. Please update your Gemfile or run: gem install ruby-openai -v '>= 8.0'"
357
+ end
358
+ rescue LoadError
359
+ # ruby-openai is not installed, which is fine - it will be caught later when trying to use it
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This hook is called when Claude Code starts a session
5
+ # It saves the transcript path for the main instance so the orchestrator can tail it
6
+
7
+ require "json"
8
+ require "fileutils"
9
+
10
+ # Read input from stdin
11
+ begin
12
+ stdin_data = $stdin.read
13
+ input = JSON.parse(stdin_data)
14
+ rescue => e
15
+ # Return error response
16
+ puts JSON.generate({
17
+ "success" => false,
18
+ "error" => "Failed to read/parse input: #{e.message}",
19
+ })
20
+ exit(1)
21
+ end
22
+
23
+ # Get session path from command-line argument or environment
24
+ session_path = ARGV[0] || ENV["CLAUDE_SWARM_SESSION_PATH"]
25
+
26
+ if session_path && input["transcript_path"]
27
+ # Write the transcript path to a known location
28
+ path_file = File.join(session_path, "main_instance_transcript.path")
29
+ File.write(path_file, input["transcript_path"])
30
+
31
+ # Return success
32
+ puts JSON.generate({
33
+ "success" => true,
34
+ })
35
+ else
36
+ # Return error if missing required data
37
+ puts JSON.generate({
38
+ "success" => false,
39
+ "error" => "Missing session path or transcript path",
40
+ })
41
+ exit(1)
42
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ # Centralized JSON handling for the Claude Swarm codebase
5
+ class JsonHandler
6
+ class << self
7
+ # Parse JSON string into Ruby object
8
+ # @param json_string [String] The JSON string to parse
9
+ # @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
10
+ # @return [Object] The parsed Ruby object, or original string if parsing fails and raise_on_error is false
11
+ # @raise [JSON::ParserError] If the JSON is invalid and raise_on_error is true
12
+ def parse(json_string, raise_on_error: false)
13
+ JSON.parse(json_string)
14
+ rescue JSON::ParserError => e
15
+ raise e if raise_on_error
16
+
17
+ json_string
18
+ end
19
+
20
+ # Parse JSON string with exception raising
21
+ # @param json_string [String] The JSON string to parse
22
+ # @return [Object] The parsed Ruby object
23
+ # @raise [JSON::ParserError] If the JSON is invalid
24
+ def parse!(json_string)
25
+ parse(json_string, raise_on_error: true)
26
+ end
27
+
28
+ # Parse JSON from a file with exception raising
29
+ # @param file_path [String] Path to the JSON file
30
+ # @return [Object] The parsed Ruby object
31
+ # @raise [Errno::ENOENT] If the file does not exist
32
+ # @raise [JSON::ParserError] If the file contains invalid JSON
33
+ def parse_file!(file_path)
34
+ content = File.read(file_path)
35
+ parse!(content)
36
+ end
37
+
38
+ # Parse JSON from a file, returning nil on error
39
+ # @param file_path [String] Path to the JSON file
40
+ # @return [Object, nil] The parsed Ruby object or nil if file doesn't exist or contains invalid JSON
41
+ def parse_file(file_path)
42
+ parse_file!(file_path)
43
+ rescue Errno::ENOENT, JSON::ParserError
44
+ nil
45
+ end
46
+
47
+ # Generate pretty-formatted JSON string
48
+ # @param object [Object] The Ruby object to convert to JSON
49
+ # @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
50
+ # @return [String, nil] The pretty-formatted JSON string, or nil if generation fails and raise_on_error is false
51
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON and raise_on_error is true
52
+ def pretty_generate(object, raise_on_error: false)
53
+ JSON.pretty_generate(object)
54
+ rescue JSON::GeneratorError, JSON::NestingError => e
55
+ raise e if raise_on_error
56
+
57
+ nil
58
+ end
59
+
60
+ # Generate pretty-formatted JSON string with exception raising
61
+ # @param object [Object] The Ruby object to convert to JSON
62
+ # @return [String] The pretty-formatted JSON string
63
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON
64
+ def pretty_generate!(object)
65
+ pretty_generate(object, raise_on_error: true)
66
+ end
67
+
68
+ # Write Ruby object to a JSON file with pretty formatting
69
+ # @param file_path [String] Path to the JSON file
70
+ # @param object [Object] The Ruby object to write
71
+ # @return [Boolean] True if successful, false if generation or write fails
72
+ def write_file(file_path, object)
73
+ json_string = pretty_generate!(object)
74
+ File.write(file_path, json_string)
75
+ true
76
+ rescue JSON::GeneratorError, JSON::NestingError, SystemCallError
77
+ false
78
+ end
79
+
80
+ # Write Ruby object to a JSON file with exception raising
81
+ # @param file_path [String] Path to the JSON file
82
+ # @param object [Object] The Ruby object to write
83
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON
84
+ # @raise [SystemCallError] If the file cannot be written
85
+ def write_file!(file_path, object)
86
+ json_string = pretty_generate!(object)
87
+ File.write(file_path, json_string)
88
+ end
89
+ end
90
+ end
91
+ end