swarm_memory 2.1.5 → 2.1.6

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,235 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # TodoWrite tool for creating and managing structured task lists
6
- #
7
- # This tool helps agents track progress on complex multi-step tasks.
8
- # Each agent maintains its own independent todo list.
9
- class TodoWrite < RubyLLM::Tool
10
- # Factory pattern: declare what parameters this tool needs for instantiation
11
- class << self
12
- def creation_requirements
13
- [:agent_name]
14
- end
15
- end
16
-
17
- description <<~DESC
18
- Use this tool to create and manage a structured task list for your current work session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
19
- It also helps the user understand the progress of the task and overall progress of their requests.
20
-
21
- ## When to Use This Tool
22
- Use this tool proactively in these scenarios:
23
-
24
- **CRITICAL**: Follow this workflow for multi-step tasks:
25
- 1. FIRST: Analyze the task scope (gather information, understand requirements)
26
- 2. SECOND: Create a COMPLETE todo list with ALL known tasks BEFORE starting work
27
- 3. THIRD: Execute tasks, marking in_progress → completed as you work
28
- 4. ONLY add new todos if unexpected work is discovered during execution
29
-
30
- Use the todo list when:
31
- 1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
32
- 2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
33
- 3. User explicitly requests todo list - When the user directly asks you to use the todo list
34
- 4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
35
- 5. After receiving new instructions - After analyzing scope, create complete todo list before starting work
36
- 6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
37
- 7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during execution
38
-
39
- ## When NOT to Use This Tool
40
-
41
- Skip using this tool when:
42
- 1. There is only a single, straightforward task
43
- 2. The task is trivial and tracking it provides no organizational benefit
44
- 3. The task can be completed in less than 3 trivial steps
45
- 4. The task is purely conversational or informational
46
-
47
- NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
48
-
49
- ## Task States and Management
50
-
51
- 1. **Task States**: Use these states to track progress:
52
- - pending: Task not yet started
53
- - in_progress: Currently working on (limit to ONE task at a time)
54
- - completed: Task finished successfully
55
-
56
- **IMPORTANT**: Task descriptions must have two forms:
57
- - content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project")
58
- - activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project")
59
-
60
- 2. **Task Management**:
61
- - Update task status in real-time as you work
62
- - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
63
- - Exactly ONE task must be in_progress at any time (not less, not more)
64
- - Complete current tasks before starting new ones
65
- - Remove tasks that are no longer relevant from the list entirely
66
- - **CRITICAL**: You MUST complete ALL pending todos before giving your final answer to the user
67
- - NEVER leave in_progress or pending tasks when you finish responding
68
-
69
- 3. **Task Completion Requirements**:
70
- - ONLY mark a task as completed when you have FULLY accomplished it
71
- - If you encounter errors, blockers, or cannot finish, keep the task as in_progress
72
- - When blocked, create a new task describing what needs to be resolved
73
- - Never mark a task as completed if:
74
- - Tests are failing
75
- - Implementation is partial
76
- - You encountered unresolved errors
77
- - You couldn't find necessary files or dependencies
78
-
79
- 4. **Task Breakdown**:
80
- - Create specific, actionable items
81
- - Break complex tasks into smaller, manageable steps
82
- - Use clear, descriptive task names
83
- - Always provide both forms (content and activeForm)
84
-
85
- ## Examples
86
-
87
- **Coding Tasks**:
88
- - content: "Fix authentication bug in login handler"
89
- - activeForm: "Fixing authentication bug in login handler"
90
-
91
- **Non-Coding Tasks**:
92
- - content: "Analyze customer feedback from Q4 survey"
93
- - activeForm: "Analyzing customer feedback from Q4 survey"
94
-
95
- **Research Tasks**:
96
- - content: "Research best practices for API rate limiting"
97
- - activeForm: "Researching best practices for API rate limiting"
98
-
99
- When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
100
- DESC
101
-
102
- param :todos_json,
103
- type: "string",
104
- desc: <<~DESC.chomp,
105
- JSON array of todo objects. Each todo must have:
106
- content (string, task in imperative form like 'Run tests'),
107
- status (string, one of: 'pending', 'in_progress', 'completed'),
108
- activeForm (string, task in present continuous form like 'Running tests').
109
- Example: [{"content":"Read file","status":"pending","activeForm":"Reading file"}]
110
- DESC
111
- required: true
112
-
113
- # Initialize the TodoWrite tool for a specific agent
114
- #
115
- # @param agent_name [Symbol, String] The agent identifier
116
- def initialize(agent_name:)
117
- super()
118
- @agent_name = agent_name.to_sym
119
- end
120
-
121
- # Override name to return simple "TodoWrite" instead of full class path
122
- def name
123
- "TodoWrite"
124
- end
125
-
126
- def execute(todos_json:)
127
- # Parse JSON
128
- todos = begin
129
- JSON.parse(todos_json)
130
- rescue JSON::ParserError
131
- nil
132
- end
133
-
134
- return validation_error("Invalid JSON format. Please provide a valid JSON array of todo objects.") if todos.nil?
135
-
136
- # Validate todos structure
137
- unless todos.is_a?(Array)
138
- return validation_error("todos must be an array of todo objects")
139
- end
140
-
141
- if todos.empty?
142
- return validation_error("todos array cannot be empty")
143
- end
144
-
145
- validated_todos = []
146
- errors = []
147
-
148
- todos.each_with_index do |todo, index|
149
- unless todo.is_a?(Hash)
150
- errors << "Todo at index #{index} must be a hash/object"
151
- next
152
- end
153
-
154
- # Convert string keys to symbols for consistency
155
- todo = todo.transform_keys(&:to_sym) if todo.is_a?(Hash)
156
-
157
- # Validate required fields
158
- unless todo[:content]
159
- errors << "Todo at index #{index} missing required field 'content'"
160
- next
161
- end
162
-
163
- unless todo[:status]
164
- errors << "Todo at index #{index} missing required field 'status'"
165
- next
166
- end
167
-
168
- unless todo[:activeForm]
169
- errors << "Todo at index #{index} missing required field 'activeForm'"
170
- next
171
- end
172
-
173
- # Validate status values
174
- valid_statuses = ["pending", "in_progress", "completed"]
175
- unless valid_statuses.include?(todo[:status].to_s)
176
- errors << "Todo at index #{index} has invalid status '#{todo[:status]}'. Must be one of: #{valid_statuses.join(", ")}"
177
- next
178
- end
179
-
180
- # Validate content and activeForm are non-empty
181
- if todo[:content].to_s.strip.empty?
182
- errors << "Todo at index #{index} has empty content"
183
- next
184
- end
185
-
186
- if todo[:activeForm].to_s.strip.empty?
187
- errors << "Todo at index #{index} has empty activeForm"
188
- next
189
- end
190
-
191
- validated_todos << {
192
- content: todo[:content].to_s,
193
- status: todo[:status].to_s,
194
- activeForm: todo[:activeForm].to_s,
195
- }
196
- end
197
-
198
- return validation_error("TodoWrite failed due to the following issues:\n#{errors.join("\n")}") unless errors.empty?
199
-
200
- # Check that exactly one task is in_progress (with helpful message)
201
- in_progress_count = validated_todos.count { |t| t[:status] == "in_progress" }
202
- warning_message = if in_progress_count == 0
203
- "Warning: No tasks marked as in_progress. You should have exactly ONE task in_progress at a time.\n" \
204
- "Please mark the task you're currently working on as in_progress.\n\n"
205
- elsif in_progress_count > 1
206
- "Warning: Multiple tasks marked as in_progress (#{in_progress_count} tasks).\n" \
207
- "You should have exactly ONE task in_progress at a time.\n" \
208
- "Please ensure only the current task is in_progress, others should be pending or completed.\n\n"
209
- else
210
- ""
211
- end
212
-
213
- # Store the validated todos
214
- Stores::TodoManager.set_todos(@agent_name, validated_todos)
215
-
216
- <<~RESPONSE
217
- <system-reminder>
218
- #{warning_message}Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:
219
- #{validated_todos.map { |t| "- #{t[:content]} (#{t[:status]})" }.join("\n")}
220
- Keep going with the tasks at hand if applicable.
221
- </system-reminder>
222
- RESPONSE
223
- rescue StandardError => e
224
- "Error managing todos: #{e.class.name} - #{e.message}"
225
- end
226
-
227
- private
228
-
229
- # Helper method for validation errors
230
- def validation_error(message)
231
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
232
- end
233
- end
234
- end
235
- end
@@ -1,262 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # WebFetch tool for fetching and processing web content
6
- #
7
- # Fetches content from URLs, converts HTML to markdown, and processes it
8
- # using an AI model to extract information based on a provided prompt.
9
- class WebFetch < RubyLLM::Tool
10
- def initialize
11
- super()
12
- @cache = {}
13
- @cache_ttl = 900 # 15 minutes in seconds
14
- @llm_enabled = SwarmSDK.settings.webfetch_llm_enabled?
15
- end
16
-
17
- def name
18
- "WebFetch"
19
- end
20
-
21
- description <<~DESC
22
- - Fetches content from a specified URL and converts it to markdown
23
- - Optionally processes the content with an LLM if configured
24
- - Fetches the URL content, converts HTML to markdown
25
- - Returns markdown content or LLM analysis (based on configuration)
26
- - Use this tool when you need to retrieve and analyze web content
27
-
28
- Usage notes:
29
- - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
30
- - The URL must be a fully-formed valid URL
31
- - HTTP URLs will be automatically upgraded to HTTPS
32
- - This tool is read-only and does not modify any files
33
- - Content will be truncated if very large
34
- - Includes a self-cleaning 15-minute cache for faster responses
35
- - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
36
-
37
- LLM Processing:
38
- - When SwarmSDK is configured with webfetch_provider and webfetch_model, the 'prompt' parameter is required
39
- - The tool will process the markdown content with the configured LLM using your prompt
40
- - Without this configuration, the tool returns raw markdown and the 'prompt' parameter is optional (ignored if provided)
41
- - Configure with: SwarmSDK.configure { |c| c.webfetch_provider = "anthropic"; c.webfetch_model = "claude-3-5-haiku-20241022" }
42
- DESC
43
-
44
- param :url,
45
- type: "string",
46
- desc: "The URL to fetch content from",
47
- required: true
48
-
49
- param :prompt,
50
- type: "string",
51
- desc: "The prompt to run on the fetched content. Required when SwarmSDK is configured with webfetch_provider and webfetch_model. Optional otherwise (ignored if LLM processing not configured).",
52
- required: false
53
-
54
- # Backward compatibility aliases - use Defaults module for new code
55
- MAX_CONTENT_LENGTH = Defaults::Limits::WEB_FETCH_CHARACTERS
56
- USER_AGENT = "SwarmSDK WebFetch Tool (https://github.com/parruda/claude-swarm)"
57
- TIMEOUT = Defaults::Timeouts::WEB_FETCH_SECONDS
58
-
59
- def execute(url:, prompt: nil)
60
- # Validate inputs
61
- return validation_error("url is required") if url.nil? || url.empty?
62
-
63
- # Validate prompt when LLM processing is enabled
64
- if @llm_enabled && (prompt.nil? || prompt.empty?)
65
- return validation_error("prompt is required when LLM processing is configured")
66
- end
67
-
68
- # Validate and normalize URL
69
- normalized_url = normalize_url(url)
70
- return validation_error("Invalid URL format: #{url}") unless normalized_url
71
-
72
- # Check cache first (cache key includes prompt if LLM is enabled)
73
- cache_key = @llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
74
- cached = get_from_cache(cache_key)
75
- return cached if cached
76
-
77
- # Fetch the URL
78
- fetch_result = fetch_url(normalized_url)
79
- return fetch_result if fetch_result.is_a?(String) && fetch_result.start_with?("Error")
80
-
81
- # Check for redirects to different hosts
82
- if fetch_result[:redirect_url] && different_host?(normalized_url, fetch_result[:redirect_url])
83
- return format_redirect_message(fetch_result[:redirect_url])
84
- end
85
-
86
- # Convert HTML to markdown
87
- markdown_content = html_to_markdown(fetch_result[:body])
88
-
89
- # Truncate if too long
90
- if markdown_content.length > MAX_CONTENT_LENGTH
91
- markdown_content = markdown_content[0...MAX_CONTENT_LENGTH]
92
- markdown_content += "\n\n[Content truncated due to length]"
93
- end
94
-
95
- # Process with AI model if LLM is enabled, otherwise return markdown
96
- result = if @llm_enabled
97
- process_with_llm(markdown_content, prompt, normalized_url)
98
- else
99
- markdown_content
100
- end
101
-
102
- # Cache the result
103
- store_in_cache(cache_key, result)
104
-
105
- result
106
- rescue StandardError => e
107
- error("Unexpected error fetching URL: #{e.class.name} - #{e.message}")
108
- end
109
-
110
- private
111
-
112
- def validation_error(message)
113
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
114
- end
115
-
116
- def error(message)
117
- "Error: #{message}"
118
- end
119
-
120
- def normalize_url(url)
121
- # Upgrade HTTP to HTTPS
122
- url = url.sub(%r{^http://}, "https://")
123
-
124
- # Validate URL format
125
- uri = URI.parse(url)
126
- return unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
127
- return unless uri.host
128
-
129
- uri.to_s
130
- rescue URI::InvalidURIError
131
- nil
132
- end
133
-
134
- def different_host?(url1, url2)
135
- uri1 = URI.parse(url1)
136
- uri2 = URI.parse(url2)
137
- uri1.host != uri2.host
138
- rescue URI::InvalidURIError
139
- false
140
- end
141
-
142
- def fetch_url(url)
143
- require "faraday"
144
- require "faraday/follow_redirects"
145
-
146
- response = Faraday.new(url: url) do |conn|
147
- conn.request(:url_encoded)
148
- conn.response(:follow_redirects, limit: 5)
149
- conn.adapter(Faraday.default_adapter)
150
- conn.options.timeout = TIMEOUT
151
- conn.options.open_timeout = TIMEOUT
152
- end.get do |req|
153
- req.headers["User-Agent"] = USER_AGENT
154
- req.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
155
- end
156
-
157
- unless response.success?
158
- return error("HTTP #{response.status}: Failed to fetch URL")
159
- end
160
-
161
- # Check final URL for redirects
162
- final_url = response.env.url.to_s
163
- redirect_url = final_url if final_url != url
164
-
165
- {
166
- body: response.body,
167
- redirect_url: redirect_url,
168
- }
169
- rescue Faraday::TimeoutError
170
- error("Request timed out after #{TIMEOUT} seconds")
171
- rescue Faraday::ConnectionFailed => e
172
- error("Connection failed: #{e.message}")
173
- rescue StandardError => e
174
- error("Failed to fetch URL: #{e.class.name} - #{e.message}")
175
- end
176
-
177
- def html_to_markdown(html)
178
- # Use HtmlConverter to handle conversion with optional reverse_markdown gem
179
- converter = DocumentConverters::HtmlConverter.new
180
- converter.convert_string(html)
181
- end
182
-
183
- def process_with_llm(content, prompt, url)
184
- # Use configured model for processing
185
- # Format the prompt to include the content
186
- full_prompt = <<~PROMPT
187
- You are analyzing content from the URL: #{url}
188
-
189
- User request: #{prompt}
190
-
191
- Content:
192
- #{content}
193
-
194
- Please respond to the user's request based on the content above.
195
- PROMPT
196
-
197
- # Get settings
198
- config = SwarmSDK.settings
199
-
200
- # Build chat with configured provider and model
201
- chat_params = {
202
- model: config.webfetch_model,
203
- provider: config.webfetch_provider.to_sym,
204
- }
205
- chat_params[:base_url] = config.webfetch_base_url if config.webfetch_base_url
206
-
207
- chat = RubyLLM.chat(**chat_params).with_params(max_tokens: config.webfetch_max_tokens)
208
-
209
- response = chat.ask(full_prompt)
210
-
211
- # Extract the text response
212
- response_text = response.content
213
- return error("Failed to process content with LLM: No response text") unless response_text
214
-
215
- response_text
216
- rescue StandardError => e
217
- error("Failed to process content with LLM: #{e.class.name} - #{e.message}")
218
- end
219
-
220
- def format_redirect_message(redirect_url)
221
- <<~MESSAGE
222
- This URL redirected to a different host.
223
-
224
- Redirect URL: #{redirect_url}
225
-
226
- <system-reminder>
227
- The requested URL redirected to a different host. To fetch the content from the redirect URL,
228
- make a new WebFetch request with the redirect URL provided above.
229
- </system-reminder>
230
- MESSAGE
231
- end
232
-
233
- def get_from_cache(key)
234
- entry = @cache[key]
235
- return unless entry
236
-
237
- # Check if cache entry is still valid
238
- if Time.now.to_i - entry[:timestamp] > @cache_ttl
239
- @cache.delete(key)
240
- return
241
- end
242
-
243
- entry[:value]
244
- end
245
-
246
- def store_in_cache(key, value)
247
- # Clean old cache entries
248
- clean_cache
249
-
250
- @cache[key] = {
251
- value: value,
252
- timestamp: Time.now.to_i,
253
- }
254
- end
255
-
256
- def clean_cache
257
- now = Time.now.to_i
258
- @cache.delete_if { |_key, entry| now - entry[:timestamp] > @cache_ttl }
259
- end
260
- end
261
- end
262
- end
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Write tool for writing content to files
6
- #
7
- # Creates new files or overwrites existing files.
8
- # Enforces read-before-write rule for existing files.
9
- # Includes validation and usage guidelines via system reminders.
10
- class Write < RubyLLM::Tool
11
- include PathResolver
12
-
13
- # Factory pattern: declare what parameters this tool needs for instantiation
14
- class << self
15
- def creation_requirements
16
- [:agent_name, :directory]
17
- end
18
- end
19
-
20
- description <<~DESC
21
- Writes a file to the local filesystem.
22
- This tool will overwrite the existing file if there is one at the provided path.
23
- If this is an existing file, you MUST use the Read tool first to read the file's contents.
24
- This tool will fail if you did not read the file first.
25
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
26
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
27
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
28
-
29
- IMPORTANT - Path Handling:
30
- - Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
31
- - Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
32
- - When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
33
- - Only use absolute paths (starting with /) when explicitly referring to system-level paths
34
- DESC
35
-
36
- param :file_path,
37
- type: "string",
38
- desc: "Path to the file. Use relative paths (e.g., 'tmp/file.txt') for files in your working directory, or absolute paths (e.g., '/etc/passwd') for system files.",
39
- required: true
40
-
41
- param :content,
42
- type: "string",
43
- desc: "The content to write to the file",
44
- required: true
45
-
46
- # Initialize the Write tool for a specific agent
47
- #
48
- # @param agent_name [Symbol, String] The agent identifier
49
- # @param directory [String] Agent's working directory
50
- def initialize(agent_name:, directory:)
51
- super()
52
- initialize_agent_context(agent_name: agent_name, directory: directory)
53
- end
54
-
55
- # Override name to return simple "Write" instead of full class path
56
- def name
57
- "Write"
58
- end
59
-
60
- def execute(file_path:, content:)
61
- # Validate inputs
62
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
63
- return validation_error("content is required") if content.nil?
64
-
65
- # CRITICAL: Resolve path against agent directory
66
- resolved_path = resolve_path(file_path)
67
-
68
- # Check if file already exists (use resolved path)
69
- file_exists = File.exist?(resolved_path)
70
-
71
- # Enforce read-before-write for existing files (use resolved path)
72
- if file_exists && !Stores::ReadTracker.file_read?(@agent_name, resolved_path)
73
- return validation_error(
74
- "Cannot write to existing file without reading it first. " \
75
- "You must use the Read tool on '#{file_path}' before overwriting it. " \
76
- "This ensures you have context about the file's current contents.",
77
- )
78
- end
79
-
80
- # Create parent directory if it doesn't exist (use resolved path)
81
- parent_dir = File.dirname(resolved_path)
82
- FileUtils.mkdir_p(parent_dir) unless File.directory?(parent_dir)
83
-
84
- # Write the file (use resolved path)
85
- File.write(resolved_path, content, encoding: "UTF-8")
86
-
87
- # Build success message
88
- byte_size = content.bytesize
89
- line_count = content.lines.count
90
- action = file_exists ? "overwrote" : "created"
91
-
92
- message = "Successfully #{action} file: #{file_path} (#{line_count} lines, #{byte_size} bytes)"
93
-
94
- # Add system reminder for overwritten files
95
- if file_exists
96
- reminder = "<system-reminder>You overwrote an existing file. Make sure this was intentional and that you read the file first if you needed to preserve any content.</system-reminder>"
97
- "#{message}\n\n#{reminder}"
98
- else
99
- message
100
- end
101
- rescue Errno::EACCES
102
- error("Permission denied: Cannot write to file '#{file_path}'")
103
- rescue Errno::EISDIR
104
- error("Path is a directory, not a file.")
105
- rescue Errno::ENOENT => e
106
- error("Failed to create parent directory: #{e.message}")
107
- rescue StandardError => e
108
- error("Unexpected error writing file: #{e.class.name} - #{e.message}")
109
- end
110
- end
111
- end
112
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- # Shared utility methods for SwarmSDK
5
- module Utils
6
- class << self
7
- # Recursively convert all hash keys to symbols
8
- #
9
- # Handles nested hashes and arrays containing hashes.
10
- #
11
- # @param obj [Object] Object to symbolize (Hash, Array, or other)
12
- # @return [Object] Object with symbolized keys (if applicable)
13
- #
14
- # @example
15
- # Utils.symbolize_keys({ "name" => "test", "config" => { "key" => "value" } })
16
- # # => { name: "test", config: { key: "value" } }
17
- def symbolize_keys(obj)
18
- case obj
19
- when Hash
20
- obj.transform_keys(&:to_sym).transform_values { |v| symbolize_keys(v) }
21
- when Array
22
- obj.map { |item| symbolize_keys(item) }
23
- else
24
- obj
25
- end
26
- end
27
-
28
- # Recursively convert all hash keys to strings
29
- #
30
- # Handles nested hashes and arrays containing hashes.
31
- #
32
- # @param obj [Object] Object to stringify (Hash, Array, or other)
33
- # @return [Object] Object with stringified keys (if applicable)
34
- #
35
- # @example
36
- # Utils.stringify_keys({ name: "test", config: { key: "value" } })
37
- # # => { "name" => "test", "config" => { "key" => "value" } }
38
- def stringify_keys(obj)
39
- case obj
40
- when Hash
41
- obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
42
- when Array
43
- obj.map { |item| stringify_keys(item) }
44
- else
45
- obj
46
- end
47
- end
48
-
49
- # Convert hash to YAML string
50
- #
51
- # Converts a Ruby hash to a YAML string. Useful for creating inline
52
- # swarm definitions from hash configurations.
53
- #
54
- # @param hash [Hash] Hash to convert
55
- # @return [String] YAML string representation
56
- #
57
- # @example
58
- # config = { version: 2, swarm: { name: "Test" } }
59
- # Utils.hash_to_yaml(config)
60
- # # => "---\nversion: 2\nswarm:\n name: Test\n"
61
- def hash_to_yaml(hash)
62
- # Convert symbols to strings for valid YAML
63
- stringified = stringify_keys(hash)
64
- stringified.to_yaml
65
- end
66
- end
67
- end
68
- end