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,712 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class CLI < Thor
5
+ include SystemUtils
6
+
7
+ class << self
8
+ def exit_on_failure?
9
+ true
10
+ end
11
+ end
12
+
13
+ desc "start [CONFIG_FILE]", "Start a Claude Swarm from configuration file"
14
+ method_option :vibe,
15
+ type: :boolean,
16
+ default: false,
17
+ desc: "Run with --dangerously-skip-permissions for all instances"
18
+ method_option :prompt,
19
+ aliases: "-p",
20
+ type: :string,
21
+ desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
22
+ method_option :interactive,
23
+ aliases: "-i",
24
+ type: :string,
25
+ desc: "Initial prompt for interactive mode"
26
+ method_option :stream_logs,
27
+ type: :boolean,
28
+ default: false,
29
+ desc: "Stream session logs to stdout (only works with -p)"
30
+ method_option :debug,
31
+ type: :boolean,
32
+ default: false,
33
+ desc: "Enable debug output"
34
+ method_option :worktree,
35
+ type: :string,
36
+ aliases: "-w",
37
+ desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
38
+ banner: "[NAME]"
39
+ method_option :session_id,
40
+ type: :string,
41
+ desc: "Use a specific session ID instead of generating one"
42
+ method_option :root_dir,
43
+ type: :string,
44
+ desc: "Root directory for resolving relative paths (defaults to current directory)"
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)
49
+
50
+ # Resolve config path relative to root directory
51
+ config_path = config_file || "claude-swarm.yml"
52
+ config_path = File.expand_path(config_path, root_dir)
53
+
54
+ unless File.exist?(config_path)
55
+ error("Configuration file not found: #{config_path}")
56
+ exit(1)
57
+ end
58
+
59
+ say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
60
+
61
+ # Validate stream_logs option
62
+ if options[:stream_logs] && !options[:prompt]
63
+ error("--stream-logs can only be used with -p/--prompt")
64
+ exit(1)
65
+ end
66
+
67
+ # Validate conflicting options
68
+ if options[:prompt] && options[:interactive]
69
+ error("Cannot use both -p/--prompt and -i/--interactive")
70
+ exit(1)
71
+ end
72
+
73
+ begin
74
+ config = Configuration.new(config_path, base_dir: ClaudeSwarm.root_dir, options: options)
75
+ generator = McpGenerator.new(config, vibe: options[:vibe])
76
+ orchestrator = Orchestrator.new(
77
+ config,
78
+ generator,
79
+ vibe: options[:vibe],
80
+ prompt: options[:prompt],
81
+ interactive_prompt: options[:interactive],
82
+ stream_logs: options[:stream_logs],
83
+ debug: options[:debug],
84
+ worktree: options[:worktree],
85
+ session_id: options[:session_id],
86
+ )
87
+ orchestrator.start
88
+ rescue Error => e
89
+ error(e.message)
90
+ exit(1)
91
+ rescue StandardError => e
92
+ error("Unexpected error: #{e.message}")
93
+ error(e.backtrace.join("\n")) if options[:verbose]
94
+ exit(1)
95
+ end
96
+ end
97
+
98
+ desc "mcp-serve", "Start an MCP server for a Claude instance"
99
+ method_option :name,
100
+ aliases: "-n",
101
+ type: :string,
102
+ required: true,
103
+ desc: "Instance name"
104
+ method_option :directory,
105
+ aliases: "-d",
106
+ type: :string,
107
+ required: true,
108
+ desc: "Working directory for the instance"
109
+ method_option :directories,
110
+ type: :array,
111
+ desc: "All directories (including main directory) for the instance"
112
+ method_option :model,
113
+ aliases: "-m",
114
+ type: :string,
115
+ required: true,
116
+ desc: "Claude model to use (e.g., opus, sonnet)"
117
+ method_option :prompt,
118
+ aliases: "-p",
119
+ type: :string,
120
+ desc: "System prompt for the instance"
121
+ method_option :description,
122
+ type: :string,
123
+ desc: "Description of the instance's role"
124
+ method_option :allowed_tools,
125
+ aliases: "-t",
126
+ type: :array,
127
+ desc: "Allowed tools for the instance"
128
+ method_option :disallowed_tools,
129
+ type: :array,
130
+ desc: "Disallowed tools for the instance"
131
+ method_option :connections,
132
+ type: :array,
133
+ desc: "Connections to other instances"
134
+ method_option :mcp_config_path,
135
+ type: :string,
136
+ desc: "Path to MCP configuration file"
137
+ method_option :debug,
138
+ type: :boolean,
139
+ default: false,
140
+ desc: "Enable debug output"
141
+ method_option :vibe,
142
+ type: :boolean,
143
+ default: false,
144
+ desc: "Run with --dangerously-skip-permissions"
145
+ method_option :calling_instance,
146
+ type: :string,
147
+ required: true,
148
+ desc: "Name of the instance that launched this MCP server"
149
+ method_option :calling_instance_id,
150
+ type: :string,
151
+ desc: "Unique ID of the instance that launched this MCP server"
152
+ method_option :instance_id,
153
+ type: :string,
154
+ desc: "Unique ID of this instance"
155
+ method_option :claude_session_id,
156
+ type: :string,
157
+ desc: "Claude session ID to resume"
158
+ method_option :provider,
159
+ type: :string,
160
+ desc: "Provider to use (claude or openai)"
161
+ method_option :temperature,
162
+ type: :numeric,
163
+ desc: "Temperature for OpenAI models"
164
+ method_option :api_version,
165
+ type: :string,
166
+ desc: "API version for OpenAI (chat_completion or responses)"
167
+ method_option :openai_token_env,
168
+ type: :string,
169
+ desc: "Environment variable name for OpenAI API key"
170
+ method_option :base_url,
171
+ type: :string,
172
+ desc: "Base URL for OpenAI API"
173
+ method_option :reasoning_effort,
174
+ type: :string,
175
+ desc: "Reasoning effort for OpenAI models"
176
+ def mcp_serve
177
+ # Validate reasoning_effort if provided
178
+ if options[:reasoning_effort]
179
+ # Only validate if provider is openai (or not specified, since it could be set elsewhere)
180
+ if options[:provider] && options[:provider] != "openai"
181
+ error("reasoning_effort is only supported for OpenAI models")
182
+ exit(1)
183
+ end
184
+
185
+ # Validate it's used with an o-series model
186
+ model = options[:model]
187
+ unless model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
188
+ error("reasoning_effort is only supported for o-series models (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)")
189
+ error("Current model: #{model}")
190
+ exit(1)
191
+ end
192
+
193
+ # Validate the value
194
+ unless ClaudeSwarm::Configuration::VALID_REASONING_EFFORTS.include?(options[:reasoning_effort])
195
+ error("reasoning_effort must be 'low', 'medium', or 'high'")
196
+ exit(1)
197
+ end
198
+ end
199
+
200
+ # Validate temperature is not used with o-series models
201
+ if options[:temperature] && options[:provider] == "openai"
202
+ model = options[:model]
203
+ if model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
204
+ error("temperature parameter is not supported for o-series models (#{model})")
205
+ error("O-series models use deterministic reasoning and don't accept temperature settings")
206
+ exit(1)
207
+ end
208
+ end
209
+
210
+ instance_config = {
211
+ name: options[:name],
212
+ directory: options[:directory],
213
+ directories: options[:directories] || [options[:directory]],
214
+ model: options[:model],
215
+ prompt: options[:prompt],
216
+ description: options[:description],
217
+ allowed_tools: options[:allowed_tools] || [],
218
+ disallowed_tools: options[:disallowed_tools] || [],
219
+ connections: options[:connections] || [],
220
+ mcp_config_path: options[:mcp_config_path],
221
+ vibe: options[:vibe] || false,
222
+ instance_id: options[:instance_id],
223
+ claude_session_id: options[:claude_session_id],
224
+ provider: options[:provider],
225
+ temperature: options[:temperature],
226
+ api_version: options[:api_version],
227
+ openai_token_env: options[:openai_token_env],
228
+ base_url: options[:base_url],
229
+ reasoning_effort: options[:reasoning_effort],
230
+ }
231
+
232
+ begin
233
+ server = ClaudeMcpServer.new(
234
+ instance_config,
235
+ calling_instance: options[:calling_instance],
236
+ calling_instance_id: options[:calling_instance_id],
237
+ debug: options[:debug],
238
+ )
239
+ server.start
240
+ rescue StandardError => e
241
+ error("Error starting MCP server: #{e.message}")
242
+ error(e.backtrace.join("\n")) if options[:debug]
243
+ exit(1)
244
+ end
245
+ end
246
+
247
+ desc "init", "Initialize a new claude-swarm.yml configuration file"
248
+ method_option :force,
249
+ aliases: "-f",
250
+ type: :boolean,
251
+ default: false,
252
+ desc: "Overwrite existing configuration file"
253
+ def init
254
+ config_path = "claude-swarm.yml"
255
+
256
+ if File.exist?(config_path) && !options[:force]
257
+ error("Configuration file already exists: #{config_path}")
258
+ error("Use --force to overwrite")
259
+ exit(1)
260
+ end
261
+
262
+ template = <<~YAML
263
+ version: 1
264
+ swarm:
265
+ name: "Swarm Name"
266
+ main: lead_developer
267
+ # before: # Optional: commands to run before launching swarm (executed in sequence)
268
+ # - "echo 'Setting up environment...'"
269
+ # - "npm install"
270
+ # - "docker-compose up -d"
271
+ instances:
272
+ lead_developer:
273
+ description: "Lead developer who coordinates the team and makes architectural decisions"
274
+ directory: .
275
+ model: sonnet
276
+ prompt: |
277
+ You are the lead developer coordinating the team
278
+ allowed_tools: [Read, Edit, Bash, Write]
279
+ # connections: [frontend_dev, backend_dev]
280
+
281
+ # Example instances (uncomment and modify as needed):
282
+
283
+ # frontend_dev:
284
+ # description: "Frontend developer specializing in React and modern web technologies"
285
+ # directory: ./frontend
286
+ # model: sonnet
287
+ # prompt: |
288
+ # You specialize in frontend development with React, TypeScript, and modern web technologies
289
+ # allowed_tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
290
+
291
+ # backend_dev:
292
+ # description: |
293
+ # Backend developer focusing on APIs, databases, and server architecture
294
+ # directory: ../other-app/backend
295
+ # model: sonnet
296
+ # prompt: |
297
+ # You specialize in backend development, APIs, databases, and server architecture
298
+ # allowed_tools: [Read, Edit, Write, Bash]
299
+
300
+ # devops_engineer:
301
+ # description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
302
+ # directory: .
303
+ # model: sonnet
304
+ # prompt: |
305
+ # You specialize in infrastrujcture, CI/CD, containerization, and deployment
306
+ # allowed_tools: [Read, Edit, Write, "Bash(docker:*)", "Bash(kubectl:*)", "Bash(terraform:*)"]
307
+
308
+ # qa_engineer:
309
+ # description: "QA engineer ensuring quality through comprehensive testing"
310
+ # directory: ./tests
311
+ # model: sonnet
312
+ # prompt: |
313
+ # You specialize in testing, quality assurance, and test automation
314
+ # allowed_tools: [Read, Edit, Write, Bash]
315
+ YAML
316
+
317
+ File.write(config_path, template)
318
+ say("Created #{config_path}", :green)
319
+ say("Edit this file to configure your swarm, then run 'claude-swarm' to start")
320
+ end
321
+
322
+ desc "generate", "Launch Claude to help generate a swarm configuration interactively"
323
+ method_option :output,
324
+ aliases: "-o",
325
+ type: :string,
326
+ desc: "Output file path for the generated configuration"
327
+ method_option :model,
328
+ aliases: "-m",
329
+ type: :string,
330
+ default: "sonnet",
331
+ desc: "Claude model to use for generation"
332
+ def generate
333
+ # Check if claude command exists
334
+ begin
335
+ system!("command -v claude > /dev/null 2>&1")
336
+ rescue Error
337
+ error("Claude CLI is not installed or not in PATH")
338
+ error("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
339
+ exit(1)
340
+ end
341
+
342
+ # Read README for context about claude-swarm capabilities
343
+ readme_path = File.join(__dir__, "../../README.md")
344
+ readme_content = File.exist?(readme_path) ? File.read(readme_path) : ""
345
+
346
+ # Build the pre-prompt
347
+ preprompt = build_generation_prompt(readme_content, options[:output])
348
+
349
+ # Launch Claude in interactive mode with the initial prompt
350
+ cmd = [
351
+ "claude",
352
+ "--model",
353
+ options[:model],
354
+ preprompt,
355
+ ]
356
+
357
+ # Execute and let the user take over
358
+ exec(*cmd)
359
+ end
360
+
361
+ desc "version", "Show Claude Swarm version"
362
+ def version
363
+ say("Claude Swarm #{VERSION}")
364
+ end
365
+
366
+ desc "ps", "List running Claude Swarm sessions"
367
+ def ps
368
+ Commands::Ps.new.execute
369
+ end
370
+
371
+ desc "show SESSION_ID", "Show detailed session information"
372
+ def show(session_id)
373
+ Commands::Show.new.execute(session_id)
374
+ end
375
+
376
+ desc "clean", "Remove stale session symlinks and orphaned worktrees"
377
+ method_option :days,
378
+ aliases: "-d",
379
+ type: :numeric,
380
+ default: 7,
381
+ desc: "Remove sessions older than N days"
382
+ def clean
383
+ # Clean stale symlinks
384
+ cleaned_symlinks = clean_stale_symlinks(options[:days])
385
+
386
+ # Clean orphaned worktrees
387
+ cleaned_worktrees = clean_orphaned_worktrees(options[:days])
388
+
389
+ if cleaned_symlinks.positive? || cleaned_worktrees.positive?
390
+ say("Cleaned #{cleaned_symlinks} stale symlink#{"s" unless cleaned_symlinks == 1}", :green)
391
+ say("Cleaned #{cleaned_worktrees} orphaned worktree#{"s" unless cleaned_worktrees == 1}", :green)
392
+ else
393
+ say("No cleanup needed", :green)
394
+ end
395
+ end
396
+
397
+ desc "restore SESSION_ID", "Restore a previous session by ID"
398
+ def restore(session_id)
399
+ restore_session(session_id)
400
+ end
401
+
402
+ desc "watch SESSION_ID", "Watch session logs"
403
+ method_option :lines,
404
+ aliases: "-n",
405
+ type: :numeric,
406
+ default: 100,
407
+ desc: "Number of lines to show initially"
408
+ def watch(session_id)
409
+ # Find session path
410
+ run_symlink = ClaudeSwarm.joined_run_dir(session_id)
411
+ session_path = if File.symlink?(run_symlink)
412
+ File.readlink(run_symlink)
413
+ else
414
+ # Search in sessions directory
415
+ Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
416
+ File.basename(path) == session_id
417
+ end
418
+ end
419
+
420
+ unless session_path && Dir.exist?(session_path)
421
+ error("Session not found: #{session_id}")
422
+ exit(1)
423
+ end
424
+
425
+ log_file = File.join(session_path, "session.log")
426
+ unless File.exist?(log_file)
427
+ error("Log file not found for session: #{session_id}")
428
+ exit(1)
429
+ end
430
+
431
+ exec("tail", "-f", "-n", options[:lines].to_s, log_file)
432
+ end
433
+
434
+ desc "list-sessions", "List all available Claude Swarm sessions"
435
+ method_option :limit,
436
+ aliases: "-l",
437
+ type: :numeric,
438
+ default: 10,
439
+ desc: "Maximum number of sessions to display"
440
+ def list_sessions
441
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
442
+ unless Dir.exist?(sessions_dir)
443
+ say("No sessions found", :yellow)
444
+ return
445
+ end
446
+
447
+ # Find all sessions with MCP configs
448
+ sessions = []
449
+ Dir.glob("#{sessions_dir}/*/*/*.mcp.json").each do |mcp_path|
450
+ session_path = File.dirname(mcp_path)
451
+ session_id = File.basename(session_path)
452
+ project_name = File.basename(File.dirname(session_path))
453
+
454
+ # Skip if we've already processed this session
455
+ next if sessions.any? { |s| s[:path] == session_path }
456
+
457
+ # Try to load session info
458
+ config_file = File.join(session_path, "config.yml")
459
+ next unless File.exist?(config_file)
460
+
461
+ # Load the config to get swarm info
462
+ begin
463
+ config_data = YamlLoader.load_config_file(config_file)
464
+ swarm_name = config_data.dig("swarm", "name") || "Unknown"
465
+ main_instance = config_data.dig("swarm", "main") || "Unknown"
466
+ rescue ClaudeSwarm::Error => e
467
+ # Warn about corrupted config files but continue
468
+ say_error("⚠️ Skipping session #{session_id} - #{e.message}")
469
+ next
470
+ end
471
+
472
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
473
+
474
+ # Get creation time from directory
475
+ created_at = File.stat(session_path).ctime
476
+
477
+ sessions << {
478
+ path: session_path,
479
+ id: session_id,
480
+ project: project_name,
481
+ created_at: created_at,
482
+ main_instance: main_instance,
483
+ instances_count: mcp_files.size,
484
+ swarm_name: swarm_name,
485
+ config_path: config_file,
486
+ }
487
+ rescue StandardError
488
+ # Skip invalid manifests
489
+ next
490
+ end
491
+
492
+ if sessions.empty?
493
+ say("No sessions found", :yellow)
494
+ return
495
+ end
496
+
497
+ # Sort by creation time (newest first)
498
+ sessions.sort_by! { |s| -s[:created_at].to_i }
499
+ sessions = sessions.first(options[:limit])
500
+
501
+ # Display sessions
502
+ say("\nAvailable sessions (newest first):\n", :bold)
503
+ sessions.each do |session|
504
+ say("\n#{session[:project]}/#{session[:id]}", :green)
505
+ say(" Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}")
506
+ say(" Main: #{session[:main_instance]}")
507
+ say(" Instances: #{session[:instances_count]}")
508
+ say(" Swarm: #{session[:swarm_name]}")
509
+ say(" Config: #{session[:config_path]}", :cyan)
510
+ end
511
+
512
+ say("\nTo resume a session, run:", :bold)
513
+ say(" claude-swarm restore <session-id>", :cyan)
514
+ end
515
+
516
+ default_task :start
517
+
518
+ private
519
+
520
+ def error(message)
521
+ $stderr.puts(Thor::Shell::Color.new.set_color(message, :red))
522
+ end
523
+
524
+ def restore_session(session_id)
525
+ say("Restoring session: #{session_id}", :green)
526
+
527
+ # Find the session path
528
+ session_path = find_session_path(session_id)
529
+ unless session_path
530
+ error("Session not found: #{session_id}")
531
+ exit(1)
532
+ end
533
+
534
+ begin
535
+ # Load session info from instance ID in MCP config
536
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
537
+ if mcp_files.empty?
538
+ error("No MCP configuration files found in session")
539
+ exit(1)
540
+ end
541
+
542
+ # Load the configuration from the session directory
543
+ config_file = File.join(session_path, "config.yml")
544
+
545
+ unless File.exist?(config_file)
546
+ error("Configuration file not found in session")
547
+ exit(1)
548
+ end
549
+
550
+ # Change to the original root directory if it exists
551
+ root_dir_file = File.join(session_path, "root_directory")
552
+ if File.exist?(root_dir_file)
553
+ original_dir = File.read(root_dir_file).strip
554
+ 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]
558
+ else
559
+ error("Original directory no longer exists: #{original_dir}")
560
+ exit(1)
561
+ end
562
+ else
563
+ # If no root_directory file, use current directory
564
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = Dir.pwd
565
+ end
566
+
567
+ config = Configuration.new(config_file, base_dir: ClaudeSwarm.root_dir)
568
+
569
+ # Load session metadata if it exists to check for worktree info
570
+ session_metadata_file = File.join(session_path, "session_metadata.json")
571
+ worktree_name = nil
572
+ metadata = JsonHandler.parse_file(session_metadata_file)
573
+ if metadata && metadata["worktree"] && metadata["worktree"]["enabled"]
574
+ worktree_name = metadata["worktree"]["name"]
575
+ say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
576
+ end
577
+
578
+ # Create orchestrator with restoration mode
579
+ generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
580
+ orchestrator = Orchestrator.new(
581
+ config,
582
+ generator,
583
+ vibe: options[:vibe],
584
+ prompt: options[:prompt],
585
+ stream_logs: options[:stream_logs],
586
+ debug: options[:debug],
587
+ restore_session_path: session_path,
588
+ worktree: worktree_name,
589
+ session_id: options[:session_id],
590
+ )
591
+ orchestrator.start
592
+ rescue StandardError => e
593
+ error("Failed to restore session: #{e.message}")
594
+ error(e.backtrace.join("\n")) if options[:debug]
595
+ exit(1)
596
+ end
597
+ end
598
+
599
+ def find_session_path(session_id)
600
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
601
+
602
+ # Search for the session ID in all projects
603
+ Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
604
+ config_path = File.join(path, "config.yml")
605
+ return path if File.exist?(config_path)
606
+ end
607
+
608
+ nil
609
+ end
610
+
611
+ def clean_stale_symlinks(days)
612
+ run_dir = ClaudeSwarm.joined_run_dir
613
+ return 0 unless Dir.exist?(run_dir)
614
+
615
+ cleaned = 0
616
+ Dir.glob("#{run_dir}/*").each do |symlink|
617
+ next unless File.symlink?(symlink)
618
+
619
+ begin
620
+ # Remove if target doesn't exist (stale)
621
+ unless File.exist?(File.readlink(symlink))
622
+ File.unlink(symlink)
623
+ cleaned += 1
624
+ next
625
+ end
626
+
627
+ # Remove if older than specified days
628
+ if File.stat(symlink).mtime < Time.now - (days * 86_400)
629
+ File.unlink(symlink)
630
+ cleaned += 1
631
+ end
632
+ rescue StandardError
633
+ # Skip problematic symlinks
634
+ end
635
+ end
636
+
637
+ cleaned
638
+ end
639
+
640
+ def clean_orphaned_worktrees(days)
641
+ worktrees_dir = ClaudeSwarm.joined_worktrees_dir
642
+ return 0 unless Dir.exist?(worktrees_dir)
643
+
644
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
645
+ cleaned = 0
646
+
647
+ Dir.glob("#{worktrees_dir}/*").each do |session_worktree_dir|
648
+ session_id = File.basename(session_worktree_dir)
649
+
650
+ # Skip if session still exists
651
+ next if Dir.glob("#{sessions_dir}/*/#{session_id}").any? { |path| File.exist?(File.join(path, "config.yml")) }
652
+
653
+ # Check age of worktree directory
654
+ begin
655
+ if File.stat(session_worktree_dir).mtime < Time.now - (days * 86_400)
656
+ # Remove all git worktrees in this session directory
657
+ Dir.glob("#{session_worktree_dir}/*/*").each do |worktree_path|
658
+ next unless File.directory?(worktree_path)
659
+
660
+ # Try to find the git repo and remove the worktree properly
661
+ git_dir = File.join(worktree_path, ".git")
662
+ if File.exist?(git_dir)
663
+ # Read the gitdir file to find the repo
664
+ gitdir_content = File.read(git_dir).strip
665
+ if gitdir_content.start_with?("gitdir:")
666
+ repo_git_path = gitdir_content.sub("gitdir: ", "")
667
+ # Extract repo path from .git/worktrees path
668
+ repo_path = repo_git_path.split("/.git/worktrees/").first
669
+
670
+ # Try to remove worktree via git
671
+ system!(
672
+ "git",
673
+ "-C",
674
+ repo_path,
675
+ "worktree",
676
+ "remove",
677
+ worktree_path,
678
+ "--force",
679
+ out: File::NULL,
680
+ err: File::NULL,
681
+ )
682
+ end
683
+ end
684
+
685
+ # Force remove directory if it still exists
686
+ FileUtils.rm_rf(worktree_path)
687
+ end
688
+
689
+ # Remove the session worktree directory
690
+ FileUtils.rm_rf(session_worktree_dir)
691
+ cleaned += 1
692
+ end
693
+ rescue StandardError => e
694
+ say("Warning: Failed to clean worktree directory #{session_worktree_dir}: #{e.message}", :yellow) if options[:debug]
695
+ end
696
+ end
697
+
698
+ cleaned
699
+ end
700
+
701
+ def build_generation_prompt(readme_content, output_file)
702
+ template_path = File.expand_path("templates/generation_prompt.md.erb", __dir__)
703
+ template = File.read(template_path)
704
+ <<~PROMPT
705
+ #{ERB.new(template, trim_mode: "-").result(binding)}
706
+
707
+ Start the conversation by greeting the user and asking: 'What kind of project would you like to create a Claude Swarm for?'
708
+ Say: 'I am ready to start'
709
+ PROMPT
710
+ end
711
+ end
712
+ end