swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  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/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ module DocumentConverters
7
+ # DOCX document converter
8
+ #
9
+ # Converts DOCX files to text and extracts images.
10
+ # Requires the docx gem (which includes rubyzip).
11
+ class DocxConverter < Base
12
+ class << self
13
+ # @return [String] gem name
14
+ def gem_name
15
+ "docx"
16
+ end
17
+
18
+ # @return [String] format name
19
+ def format_name
20
+ "DOCX"
21
+ end
22
+
23
+ # @return [Array<String>] supported extensions
24
+ def extensions
25
+ [".docx"]
26
+ end
27
+ end
28
+
29
+ # Convert DOCX to text with optional image attachments
30
+ #
31
+ # @param file_path [String] path to DOCX file
32
+ # @return [String, RubyLLM::Content] text or content with images
33
+ def convert(file_path)
34
+ return unsupported_format_message unless self.class.available?
35
+ return error("Legacy .doc format not supported") if file_path.end_with?(".doc")
36
+
37
+ require "docx"
38
+ doc = Docx::Document.open(file_path)
39
+
40
+ # Extract text content
41
+ output = build_text_output(doc, file_path)
42
+
43
+ # Extract images (inline - no separate class)
44
+ image_paths = extract_images(file_path)
45
+
46
+ if image_paths.any?
47
+ content = RubyLLM::Content.new(output)
48
+ image_paths.each { |path| content.add_attachment(path) }
49
+ content
50
+ else
51
+ output
52
+ end
53
+ rescue StandardError => e
54
+ error("DOCX conversion failed: #{e.message}")
55
+ end
56
+
57
+ private
58
+
59
+ # Build text output from DOCX document
60
+ #
61
+ # @param doc [Docx::Document] opened document
62
+ # @param file_path [String] original file path
63
+ # @return [String] formatted text output
64
+ def build_text_output(doc, file_path)
65
+ output = []
66
+ output << "DOCX: #{File.basename(file_path)}"
67
+ output << "=" * 60
68
+ output << ""
69
+
70
+ # Paragraphs
71
+ doc.paragraphs.each do |para|
72
+ text = para.text.strip
73
+ output << text unless text.empty?
74
+ end
75
+
76
+ # Tables
77
+ doc.tables.each_with_index do |table, idx|
78
+ output << ""
79
+ output << "Table #{idx + 1}:"
80
+ output << "-" * 40
81
+ table.rows.each do |row|
82
+ cells = row.cells.map(&:text)
83
+ output << cells.join(" | ")
84
+ end
85
+ end
86
+
87
+ output.join("\n")
88
+ end
89
+
90
+ # Extract images from DOCX ZIP (word/media/)
91
+ #
92
+ # @param docx_path [String] path to DOCX file
93
+ # @return [Array<String>] paths to extracted image files
94
+ def extract_images(docx_path)
95
+ require "zip"
96
+ images = []
97
+ temp_dir = Dir.mktmpdir("docx_#{Process.pid}")
98
+
99
+ Zip::File.open(docx_path) do |zip|
100
+ zip.each do |entry|
101
+ next unless entry.name.start_with?("word/media/")
102
+
103
+ ext = File.extname(entry.name).downcase
104
+ next unless [".png", ".jpg", ".jpeg", ".gif"].include?(ext)
105
+
106
+ path = File.join(temp_dir, File.basename(entry.name))
107
+ entry.extract(path)
108
+ images << path
109
+ end
110
+ end
111
+
112
+ images
113
+ rescue StandardError
114
+ [] # Silently ignore extraction failures
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ module DocumentConverters
7
+ # PDF document converter
8
+ #
9
+ # Converts PDF files to text and extracts JPEG images.
10
+ # Requires the pdf-reader gem.
11
+ class PdfConverter < Base
12
+ class << self
13
+ # @return [String] gem name
14
+ def gem_name
15
+ "pdf-reader"
16
+ end
17
+
18
+ # @return [String] format name
19
+ def format_name
20
+ "PDF"
21
+ end
22
+
23
+ # @return [Array<String>] supported extensions
24
+ def extensions
25
+ [".pdf"]
26
+ end
27
+ end
28
+
29
+ # Convert PDF to text with optional image attachments
30
+ #
31
+ # @param file_path [String] path to PDF file
32
+ # @return [String, RubyLLM::Content] text or content with images
33
+ def convert(file_path)
34
+ return unsupported_format_message unless self.class.available?
35
+
36
+ require "pdf-reader"
37
+ reader = PDF::Reader.new(file_path)
38
+
39
+ # Extract text from all pages
40
+ output = build_text_output(reader, file_path)
41
+
42
+ # Extract JPEG images (inline - no separate class)
43
+ image_paths = extract_jpeg_images(reader)
44
+
45
+ # Return with images if any extracted
46
+ if image_paths.any?
47
+ content = RubyLLM::Content.new(output)
48
+ image_paths.each { |path| content.add_attachment(path) }
49
+ content
50
+ else
51
+ output
52
+ end
53
+ rescue PDF::Reader::MalformedPDFError => e
54
+ error("Malformed PDF: #{e.message}")
55
+ rescue StandardError => e
56
+ error("PDF conversion failed: #{e.message}")
57
+ end
58
+
59
+ private
60
+
61
+ # Build text output from PDF pages
62
+ #
63
+ # @param reader [PDF::Reader] initialized reader
64
+ # @param file_path [String] original file path
65
+ # @return [String] formatted text output
66
+ def build_text_output(reader, file_path)
67
+ output = []
68
+ output << "PDF: #{File.basename(file_path)}"
69
+ output << "=" * 60
70
+ output << "Pages: #{reader.page_count}"
71
+ output << ""
72
+
73
+ reader.pages.each_with_index do |page, idx|
74
+ output << "Page #{idx + 1}:"
75
+ output << "-" * 60
76
+ text = page.text.strip
77
+ output << (text.empty? ? "(No text)" : text)
78
+ output << ""
79
+ end
80
+
81
+ output.join("\n")
82
+ end
83
+
84
+ # Extract JPEG images only (LLM API compatible)
85
+ #
86
+ # @param reader [PDF::Reader] initialized reader
87
+ # @return [Array<String>] paths to extracted JPEG files
88
+ def extract_jpeg_images(reader)
89
+ images = []
90
+ temp_dir = Dir.mktmpdir("pdf_#{Process.pid}")
91
+
92
+ reader.pages.each_with_index do |page, page_num|
93
+ page.xobjects.each do |name, stream|
94
+ next unless stream.hash[:Subtype] == :Image
95
+ next unless stream.hash[:Filter] == :DCTDecode # JPEG only
96
+
97
+ path = File.join(temp_dir, "p#{page_num + 1}_#{name}.jpg")
98
+ File.binwrite(path, stream.data)
99
+ images << path
100
+ end
101
+ end
102
+
103
+ images
104
+ rescue StandardError
105
+ [] # Silently ignore extraction failures
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ module DocumentConverters
7
+ # XLSX/Spreadsheet converter
8
+ #
9
+ # Converts spreadsheet files (XLSX, XLS, ODS) to CSV format.
10
+ # Requires the roo gem (and roo-xls for legacy XLS support).
11
+ class XlsxConverter < Base
12
+ class << self
13
+ # @return [String] gem name
14
+ def gem_name
15
+ "roo"
16
+ end
17
+
18
+ # @return [String] format name
19
+ def format_name
20
+ "XLSX/Spreadsheet"
21
+ end
22
+
23
+ # @return [Array<String>] supported extensions
24
+ def extensions
25
+ [".xlsx", ".xls", ".ods"]
26
+ end
27
+ end
28
+
29
+ # Convert spreadsheet to CSV text format
30
+ #
31
+ # @param file_path [String] path to spreadsheet file
32
+ # @return [String] CSV formatted text
33
+ def convert(file_path)
34
+ return unsupported_format_message unless self.class.available?
35
+ return unsupported_xls_message if file_path.end_with?(".xls") && !xls_available?
36
+
37
+ require "roo"
38
+ require "csv"
39
+
40
+ spreadsheet = Roo::Spreadsheet.open(file_path)
41
+ build_csv_output(spreadsheet, file_path)
42
+ rescue StandardError => e
43
+ error("Spreadsheet conversion failed: #{e.message}")
44
+ end
45
+
46
+ private
47
+
48
+ # Check if roo-xls gem is available for legacy XLS support
49
+ #
50
+ # @return [Boolean] true if roo-xls is installed
51
+ def xls_available?
52
+ Gem::Specification.find_by_name("roo-xls")
53
+ true
54
+ rescue Gem::MissingSpecError
55
+ false
56
+ end
57
+
58
+ # Return system reminder for missing roo-xls gem
59
+ #
60
+ # @return [String] formatted system reminder
61
+ def unsupported_xls_message
62
+ <<~MSG.strip
63
+ <system-reminder>
64
+ Legacy XLS format requires additional gem.
65
+
66
+ To enable XLS support:
67
+ gem install roo-xls
68
+
69
+ Or save as .xlsx format.
70
+ </system-reminder>
71
+ MSG
72
+ end
73
+
74
+ # Build CSV output from spreadsheet
75
+ #
76
+ # @param spreadsheet [Roo::Spreadsheet] opened spreadsheet
77
+ # @param file_path [String] original file path
78
+ # @return [String] formatted CSV output
79
+ def build_csv_output(spreadsheet, file_path)
80
+ output = []
81
+ output << "Spreadsheet: #{File.basename(file_path)}"
82
+ output << "=" * 60
83
+ output << ""
84
+
85
+ spreadsheet.sheets.each do |sheet_name|
86
+ spreadsheet.default_sheet = sheet_name
87
+ rows = spreadsheet.last_row || 0
88
+ cols = spreadsheet.last_column || 0
89
+
90
+ output << "Sheet: #{sheet_name} (#{rows} rows × #{cols} cols)"
91
+ output << "-" * 60
92
+
93
+ # Stream rows for memory efficiency
94
+ spreadsheet.each_row_streaming do |row|
95
+ cells = row.map { |cell| format_cell(cell) }
96
+ output << CSV.generate_line(cells).chomp
97
+ end
98
+
99
+ output << ""
100
+ end
101
+
102
+ output.join("\n")
103
+ end
104
+
105
+ # Format cell based on type
106
+ #
107
+ # @param cell [Roo::Cell] cell to format
108
+ # @return [String] formatted cell value
109
+ def format_cell(cell)
110
+ return "" if cell.nil? || cell.value.nil?
111
+
112
+ case cell.type
113
+ when :string then cell.value.to_s
114
+ when :float, :number then cell.value.to_s
115
+ when :date then cell.value.strftime("%Y-%m-%d")
116
+ when :datetime then cell.value.strftime("%Y-%m-%d %H:%M:%S")
117
+ when :time then cell.value.strftime("%H:%M:%S")
118
+ when :boolean then cell.value ? "TRUE" : "FALSE"
119
+ when :formula then cell.value.to_s # Calculated value
120
+ when :percentage then "#{(cell.value * 100).round(2)}%"
121
+ else cell.value.to_s
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Edit tool for performing exact string replacements in files
7
+ #
8
+ # Uses exact string matching to find and replace content.
9
+ # Requires the file to have been read first via the Read tool.
10
+ class Edit < Base
11
+ class << self
12
+ # @return [Array<Symbol>] Constructor requirements
13
+ def creation_requirements
14
+ [:agent_name, :directory, :read_tracker]
15
+ end
16
+ end
17
+
18
+ description <<~DESC
19
+ Performs exact string replacements in files.
20
+ You must use Read on a file before editing it.
21
+ The edit will FAIL if old_string is not unique — provide more context or use replace_all.
22
+
23
+ Path handling:
24
+ - Relative paths resolve against your working directory
25
+ - Absolute paths (starting with /) are used as-is
26
+ DESC
27
+
28
+ param :file_path,
29
+ type: "string",
30
+ desc: "Path to the file to edit",
31
+ required: true
32
+
33
+ param :old_string,
34
+ type: "string",
35
+ desc: "The exact text to replace (must match exactly including whitespace)",
36
+ required: true
37
+
38
+ param :new_string,
39
+ type: "string",
40
+ desc: "The text to replace it with (must be different from old_string)",
41
+ required: true
42
+
43
+ param :replace_all,
44
+ type: "boolean",
45
+ desc: "Replace all occurrences of old_string (default false)",
46
+ required: false
47
+
48
+ # @param agent_name [Symbol, String] Agent identifier
49
+ # @param directory [String] Agent's working directory
50
+ # @param read_tracker [ReadTracker] Shared read tracker for enforcement
51
+ def initialize(agent_name:, directory:, read_tracker:)
52
+ super()
53
+ @agent_name = agent_name.to_sym
54
+ @directory = File.expand_path(directory)
55
+ @read_tracker = read_tracker
56
+ end
57
+
58
+ # Execute file edit
59
+ #
60
+ # @param file_path [String] Path to the file
61
+ # @param old_string [String] Text to find
62
+ # @param new_string [String] Replacement text
63
+ # @param replace_all [Boolean] Replace all occurrences
64
+ # @return [String] Success or error message
65
+ def execute(file_path:, old_string:, new_string:, replace_all: false)
66
+ return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
67
+ return validation_error("old_string is required") if old_string.nil? || old_string.empty?
68
+ return validation_error("new_string is required") if new_string.nil?
69
+ return validation_error("old_string and new_string must be different.") if old_string == new_string
70
+
71
+ resolved_path = resolve_path(file_path)
72
+ return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
73
+
74
+ unless @read_tracker.file_read?(@agent_name, resolved_path)
75
+ return validation_error(
76
+ "Cannot edit file without reading it first. " \
77
+ "Use the Read tool on '#{file_path}' before editing.",
78
+ )
79
+ end
80
+
81
+ content = File.read(resolved_path, encoding: "UTF-8")
82
+
83
+ unless content.include?(old_string)
84
+ return validation_error("old_string not found in file. Make sure it matches exactly, including all whitespace and indentation.")
85
+ end
86
+
87
+ occurrences = content.scan(old_string).count
88
+
89
+ if !replace_all && occurrences > 1
90
+ return validation_error(
91
+ "Found #{occurrences} occurrences of old_string. " \
92
+ "Provide more context to make the match unique, or use replace_all: true.",
93
+ )
94
+ end
95
+
96
+ new_content = replace_all ? content.gsub(old_string, new_string) : content.sub(old_string, new_string)
97
+ File.write(resolved_path, new_content, encoding: "UTF-8")
98
+
99
+ replaced_count = replace_all ? occurrences : 1
100
+ "Successfully replaced #{replaced_count} occurrence(s) in #{file_path}"
101
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
102
+ error("File contains invalid UTF-8. Cannot edit binary files.")
103
+ rescue Errno::EACCES
104
+ error("Permission denied: Cannot read or write file '#{file_path}'")
105
+ rescue StandardError => e
106
+ error("Unexpected error editing file: #{e.class.name} - #{e.message}")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Glob tool for fast file pattern matching
7
+ #
8
+ # Finds files and directories matching glob patterns, sorted by modification time.
9
+ class Glob < Base
10
+ class << self
11
+ # @return [Array<Symbol>] Constructor requirements
12
+ def creation_requirements
13
+ [:directory]
14
+ end
15
+ end
16
+
17
+ description <<~DESC
18
+ Fast file pattern matching tool.
19
+
20
+ Supports glob patterns like "**/*.js" or "src/**/*.ts".
21
+ Returns matching file paths sorted by modification time (most recent first).
22
+ DESC
23
+
24
+ param :pattern,
25
+ type: "string",
26
+ desc: "The glob pattern to match files against",
27
+ required: true
28
+
29
+ param :path,
30
+ type: "string",
31
+ desc: "Directory to search in. Defaults to working directory.",
32
+ required: false
33
+
34
+ # @param directory [String] Working directory for pattern matching
35
+ def initialize(directory:)
36
+ super()
37
+ @directory = File.expand_path(directory)
38
+ end
39
+
40
+ # Execute glob search
41
+ #
42
+ # @param pattern [String] Glob pattern
43
+ # @param path [String, nil] Search directory
44
+ # @return [String] Matching paths or error
45
+ def execute(pattern:, path: nil)
46
+ return validation_error("pattern is required") if pattern.nil? || pattern.to_s.strip.empty?
47
+
48
+ search_path = resolve_search_path(path)
49
+ return search_path if search_path.start_with?("<tool_use_error>")
50
+
51
+ full_pattern = pattern.start_with?("/") ? pattern : File.join(search_path, pattern)
52
+ matches = Dir.glob(full_pattern, File::FNM_DOTMATCH)
53
+
54
+ matches.reject! do |f|
55
+ basename = File.basename(f.chomp("/"))
56
+ basename == "." || basename == ".."
57
+ end
58
+
59
+ return "No matches found for pattern: #{pattern}" if matches.empty?
60
+
61
+ matches.sort_by! { |f| -File.mtime(f).to_i }
62
+
63
+ max_results = Configuration.instance.glob_result_limit
64
+ truncated = matches.count > max_results
65
+ matches = matches.take(max_results) if truncated
66
+
67
+ output = matches.join("\n")
68
+ output += "\n\n<system-reminder>Results limited to first #{max_results} matches.</system-reminder>" if truncated
69
+ output
70
+ rescue Errno::EACCES => e
71
+ error("Permission denied: #{e.message}")
72
+ rescue StandardError => e
73
+ error("Failed to execute glob: #{e.class.name} - #{e.message}")
74
+ end
75
+
76
+ private
77
+
78
+ # Resolve search path with validation
79
+ #
80
+ # @param path [String, nil] User-provided path
81
+ # @return [String] Resolved absolute path or validation error
82
+ def resolve_search_path(path)
83
+ if path && !path.to_s.strip.empty?
84
+ return validation_error("Invalid path value.") if ["undefined", "null"].include?(path.to_s.strip.downcase)
85
+ return validation_error("Path does not exist: #{path}") unless File.exist?(path)
86
+ return validation_error("Path is not a directory: #{path}") unless File.directory?(path)
87
+
88
+ resolve_path(path)
89
+ else
90
+ @directory
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end