shared_tools 0.2.3 → 0.3.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +594 -42
  4. data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +20 -3
  5. data/lib/shared_tools/mcp/imcp.rb +28 -0
  6. data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
  7. data/lib/shared_tools/mcp.rb +24 -0
  8. data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
  9. data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
  10. data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
  11. data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
  12. data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
  13. data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
  14. data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
  15. data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
  16. data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
  17. data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
  18. data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
  19. data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
  20. data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
  21. data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
  22. data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
  23. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
  24. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
  25. data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
  26. data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
  27. data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
  28. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
  29. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
  30. data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
  31. data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
  32. data/lib/shared_tools/tools/browser.rb +27 -0
  33. data/lib/shared_tools/tools/browser_tool.rb +255 -0
  34. data/lib/shared_tools/tools/calculator_tool.rb +169 -0
  35. data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
  36. data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
  37. data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
  38. data/lib/shared_tools/tools/computer.rb +21 -0
  39. data/lib/shared_tools/tools/computer_tool.rb +207 -0
  40. data/lib/shared_tools/tools/data_science_kit.rb +707 -0
  41. data/lib/shared_tools/tools/database/base_driver.rb +17 -0
  42. data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
  43. data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
  44. data/lib/shared_tools/tools/database.rb +9 -0
  45. data/lib/shared_tools/tools/database_query_tool.rb +313 -0
  46. data/lib/shared_tools/tools/database_tool.rb +99 -0
  47. data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
  48. data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
  49. data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
  50. data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
  51. data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
  52. data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
  53. data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
  54. data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
  55. data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
  56. data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
  57. data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
  58. data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
  59. data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
  60. data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
  61. data/lib/shared_tools/tools/disk.rb +17 -0
  62. data/lib/shared_tools/tools/disk_tool.rb +132 -0
  63. data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
  64. data/lib/shared_tools/tools/doc.rb +8 -0
  65. data/lib/shared_tools/tools/doc_tool.rb +109 -0
  66. data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
  67. data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
  68. data/lib/shared_tools/tools/docker.rb +8 -0
  69. data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
  70. data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
  71. data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
  72. data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
  73. data/lib/shared_tools/tools/eval.rb +10 -0
  74. data/lib/shared_tools/tools/eval_tool.rb +139 -0
  75. data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
  76. data/lib/shared_tools/tools/version.rb +7 -0
  77. data/lib/shared_tools/tools/weather_tool.rb +197 -0
  78. data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
  79. data/lib/shared_tools/tools.rb +16 -0
  80. data/lib/shared_tools/version.rb +1 -1
  81. data/lib/shared_tools.rb +9 -24
  82. metadata +189 -68
  83. data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
  84. data/lib/shared_tools/llm_rb.rb +0 -9
  85. data/lib/shared_tools/omniai.rb +0 -9
  86. data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
  87. data/lib/shared_tools/raix.rb +0 -9
  88. data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
  89. data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
  90. data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
  91. data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
  92. data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
  93. data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
  94. data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
  95. data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
  96. data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
  97. data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
  98. data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
  99. data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -15
  100. data/lib/shared_tools/ruby_llm/mcp.rb +0 -12
  101. data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
  102. data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
  103. data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
  104. data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
  105. data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
  106. data/lib/shared_tools/ruby_llm.rb +0 -12
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Browser
8
+ # A browser automation tool for taking screenshots of the current page.
9
+ class PageScreenshotTool < ::RubyLLM::Tool
10
+ def self.name = 'browser_page_screenshot'
11
+
12
+ description "A browser automation tool for taking screenshots of the current page."
13
+
14
+ def initialize(driver: nil, logger: nil)
15
+ @driver = driver || default_driver
16
+ @logger = logger || RubyLLM.logger
17
+ end
18
+
19
+ def execute
20
+ @logger.info("#{self.class.name}##{__method__}")
21
+
22
+ @driver.screenshot do |file|
23
+ "data:image/png;base64,#{Base64.strict_encode64(file.read)}"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def default_driver
30
+ if defined?(Watir)
31
+ WatirDriver.new(logger: @logger)
32
+ else
33
+ raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module SelectorGenerator
7
+ # Basic selector generation methods
8
+ module BaseSelectors
9
+ def placeholder_selector(element, tag)
10
+ valid_attribute?(element["placeholder"]) ? ["#{tag}[placeholder=\"#{element['placeholder']}\"]"] : []
11
+ end
12
+
13
+ def aria_label_selector(element, tag)
14
+ valid_attribute?(element["aria-label"]) ? ["#{tag}[aria-label=\"#{element['aria-label']}\"]"] : []
15
+ end
16
+
17
+ def name_selector(element, tag)
18
+ valid_attribute?(element["name"]) ? ["#{tag}[name=\"#{element['name']}\"]"] : []
19
+ end
20
+
21
+ def maxlength_selector(element, tag)
22
+ valid_attribute?(element["maxlength"]) ? ["#{tag}[maxlength=\"#{element['maxlength']}\"]"] : []
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module SelectorGenerator
7
+ # Context-aware selector generation for complex elements
8
+ module ContextualSelectors
9
+ def generate_contextual_selectors(element)
10
+ selectors = []
11
+ selectors.concat(parent_class_selectors(element))
12
+ selectors.concat(label_based_selectors(element))
13
+ selectors.concat(position_based_selectors(element))
14
+ selectors
15
+ end
16
+
17
+ # Generate selectors based on parent container classes
18
+ def parent_class_selectors(element)
19
+ significant_parent = find_significant_parent(element)
20
+ return [] unless significant_parent
21
+
22
+ parent_class = most_specific_class(significant_parent)
23
+ return [] unless parent_class
24
+
25
+ build_parent_selector(element, parent_class)
26
+ end
27
+
28
+ # Build selector with parent class context
29
+ def build_parent_selector(element, parent_class)
30
+ base = ".#{parent_class} #{element.name}"
31
+ return ["#{base}[placeholder=\"#{element['placeholder']}\"]"] if element["placeholder"]
32
+ return ["#{base}[type=\"#{element['type']}\"]"] if element["type"]
33
+
34
+ [base]
35
+ end
36
+
37
+ # Find parent with meaningful class (not generic like 'row' or 'col')
38
+ def find_significant_parent(element)
39
+ parent = element.parent
40
+ while parent && parent.name != "body"
41
+ return parent if element_has_significant_class?(parent)
42
+
43
+ parent = parent.parent
44
+ end
45
+ nil
46
+ end
47
+
48
+ # Check if element has significant class
49
+ def element_has_significant_class?(element)
50
+ classes = element["class"]&.split || []
51
+ classes.any? { |c| significant_class?(c) }
52
+ end
53
+
54
+ # Check if class name is likely to be meaningful/specific
55
+ def significant_class?(class_name)
56
+ return false if class_name.length < 4
57
+ return false if generic_class?(class_name)
58
+
59
+ class_name.match?(/[a-z]+[-_]?[a-z]+/i)
60
+ end
61
+
62
+ # Common generic class names to ignore
63
+ def generic_class?(class_name)
64
+ %w[row col container wrapper inner outer main].include?(class_name.downcase)
65
+ end
66
+
67
+ # Get most specific (longest) class name
68
+ def most_specific_class(element)
69
+ classes = element["class"]&.split || []
70
+ classes.select { |c| significant_class?(c) }.max_by(&:length)
71
+ end
72
+
73
+ # Generate selectors based on label associations
74
+ def label_based_selectors(element)
75
+ return [] unless stable_id?(element)
76
+
77
+ label = find_label_for_element(element)
78
+ label ? ["#{element.name}##{element['id']}"] : []
79
+ end
80
+
81
+ # Check if element has stable (non-React) ID
82
+ def stable_id?(element)
83
+ id = element["id"]
84
+ id && !id.empty? && !id.match?(/^:r[0-9a-z]+:$/i)
85
+ end
86
+
87
+ # Find label element associated with this element
88
+ def find_label_for_element(element)
89
+ element.document.at_css("label[for=\"#{element['id']}\"]")
90
+ end
91
+
92
+ # Generate position-based selectors for similar elements
93
+ def position_based_selectors(element)
94
+ siblings = find_similar_siblings(element)
95
+ return [] unless siblings.size > 1
96
+
97
+ index = siblings.index(element) + 1
98
+ parent_context = parent_context_prefix(element)
99
+ build_position_selector(element, index, parent_context)
100
+ end
101
+
102
+ # Build nth-of-type selector
103
+ def build_position_selector(element, index, parent_context = "")
104
+ nth = ":nth-of-type(#{index})"
105
+ base = "#{parent_context}#{element.name}#{nth}"
106
+ return ["#{parent_context}#{element.name}[type=\"#{element['type']}\"]#{nth}"] if element["type"]
107
+ if element["placeholder"]
108
+ return ["#{parent_context}#{element.name}[placeholder=\"#{element['placeholder']}\"]#{nth}"]
109
+ end
110
+
111
+ [base]
112
+ end
113
+
114
+ # Find sibling elements of same type with similar attributes
115
+ def find_similar_siblings(element)
116
+ return [] unless element.parent
117
+
118
+ element.parent.css(element.name).select { |sibling| same_key_attributes?(element, sibling) }
119
+ end
120
+
121
+ # Check if two elements have same key attributes
122
+ def same_key_attributes?(elem1, elem2)
123
+ return false unless elem1.name == elem2.name
124
+
125
+ elem1.name == "input" ? elem1["type"] == elem2["type"] : true
126
+ end
127
+
128
+ # Get parent context for more specific position selectors
129
+ def parent_context_prefix(element)
130
+ parent = find_significant_parent(element)
131
+ return "" unless parent
132
+
133
+ parent_class = most_specific_class(parent)
134
+ parent_class ? ".#{parent_class} " : ""
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ # Generates stable CSS selectors for HTML elements
7
+ module SelectorGenerator
8
+ extend BaseSelectors
9
+ extend ContextualSelectors
10
+
11
+ module_function
12
+
13
+ def generate_stable_selectors(element)
14
+ return [] unless valid_element?(element)
15
+
16
+ selectors = []
17
+ selectors.concat(generate_by_type(element))
18
+ selectors.concat(generate_contextual_selectors(element))
19
+ selectors.compact.uniq
20
+ end
21
+
22
+ def generate_by_type(element)
23
+ case element.name
24
+ when "input" then generate_input_selectors(element)
25
+ when "textarea" then generate_textarea_selectors(element)
26
+ when "select" then generate_select_selectors(element)
27
+ else []
28
+ end
29
+ end
30
+
31
+ def valid_element?(element)
32
+ element.respond_to?(:name) && element.respond_to?(:parent)
33
+ end
34
+
35
+ def generate_input_selectors(element)
36
+ selectors = []
37
+ selectors.concat(placeholder_selector(element, "input"))
38
+ selectors.concat(aria_label_selector(element, "input"))
39
+ selectors.concat(type_selectors(element))
40
+ selectors.concat(attribute_selectors(element, "input"))
41
+ selectors
42
+ end
43
+
44
+ def generate_textarea_selectors(element)
45
+ placeholder_selector(element, "textarea") + name_selector(element, "textarea")
46
+ end
47
+
48
+ def generate_select_selectors(element)
49
+ name_selector(element, "select") + aria_label_selector(element, "select")
50
+ end
51
+
52
+ def type_selectors(element)
53
+ return [] unless valid_attribute?(element["type"])
54
+
55
+ base = "input[type=\"#{element['type']}\"]"
56
+ [base, amount_class_selector(base, element)].compact
57
+ end
58
+
59
+ def amount_class_selector(base, element)
60
+ element["class"]&.include?("wv-input--amount") ? "#{base}.wv-input--amount" : nil
61
+ end
62
+
63
+ def attribute_selectors(element, tag)
64
+ maxlength_selector(element, tag) + name_selector(element, tag)
65
+ end
66
+
67
+ def valid_attribute?(attribute)
68
+ attribute && attribute.strip.length.positive?
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "nokogiri"
5
+ rescue LoadError
6
+ # Nokogiri is optional - will raise error when tool is used without it
7
+ end
8
+
9
+ module SharedTools
10
+ module Tools
11
+ module Browser
12
+ # A browser automation tool for inspecting elements using CSS selectors.
13
+ class SelectorInspectTool < ::RubyLLM::Tool
14
+ def self.name = 'browser_selector_inspect'
15
+
16
+ include InspectUtils
17
+
18
+ description "A browser automation tool for finding and inspecting elements by CSS selector."
19
+
20
+ params do
21
+ string :selector, description: "CSS selector to target specific elements"
22
+ integer :context_size, description: "Number of parent elements to include for context", required: false
23
+ end
24
+
25
+ def initialize(driver: nil, logger: nil)
26
+ @driver = driver || default_driver
27
+ @logger = logger || RubyLLM.logger
28
+ end
29
+
30
+ def execute(selector:, context_size: 2)
31
+ raise LoadError, "SelectorInspectTool requires the 'nokogiri' gem. Install it with: gem install nokogiri" unless defined?(Nokogiri)
32
+
33
+ @logger.info("#{self.class.name}##{__method__}")
34
+
35
+ doc = cleaned_document(html: @driver.html)
36
+ target_elements = doc.css(selector)
37
+
38
+ return "No elements found matching selector: #{selector}" if target_elements.empty?
39
+
40
+ format_elements(target_elements, selector, context_size)
41
+ end
42
+
43
+ private
44
+
45
+ def default_driver
46
+ if defined?(Watir)
47
+ WatirDriver.new(logger: @logger)
48
+ else
49
+ raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
50
+ end
51
+ end
52
+
53
+ def format_elements(elements, selector, context_size)
54
+ result = "Found #{elements.size} elements matching '#{selector}':\n\n"
55
+
56
+ elements.each_with_index do |element, index|
57
+ result += "--- Element #{index + 1} ---\n"
58
+ result += Formatters::ElementFormatter.get_parent_context(element, context_size) if context_size.positive?
59
+ result += "Element: #{element.to_html}\n\n"
60
+ end
61
+
62
+ result
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ # @example
7
+ # browser = Watir::Browser.new(:chrome)
8
+ # tool = SharedTools::Tools::Browser::TextFieldSetTool.new(browser:)
9
+ # tool.execute(selector: "...", text: "...")
10
+ class TextFieldAreaSetTool < ::RubyLLM::Tool
11
+ def self.name = 'browser_text_field_set'
12
+
13
+ description "A browser automation tool for clicking a specific link."
14
+
15
+ params do
16
+ string :selector, description: "The ID / name of the text field / area to interact with."
17
+ string :text, description: "The text to set."
18
+ end
19
+
20
+ def initialize(driver: nil, logger: nil)
21
+ @driver = driver || default_driver
22
+ @logger = logger || RubyLLM.logger
23
+ end
24
+
25
+ # @param selector [String] The ID / name of the text field / text area to interact with.
26
+ # @param text [String] The text to set.
27
+ def execute(selector:, text:)
28
+ @logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
29
+
30
+ @driver.fill_in(selector:, text:)
31
+ end
32
+
33
+ private
34
+
35
+ def default_driver
36
+ if defined?(Watir)
37
+ WatirDriver.new(logger: @logger)
38
+ else
39
+ raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ # @example
7
+ # driver = Selenium::WebDriver.for :chrome
8
+ # tool = SharedTools::Tools::Browser::VisitTool.new(driver:)
9
+ # tool.execute(to: "https://news.ycombinator.com")
10
+ class VisitTool < ::RubyLLM::Tool
11
+ def self.name = 'browser_visit'
12
+
13
+ description "A browser automation tool for navigating to a specific URL."
14
+
15
+ params do
16
+ string :url, description: "A URL (e.g. https://news.ycombinator.com)."
17
+ end
18
+
19
+ def initialize(driver: nil, logger: nil)
20
+ @driver = driver || default_driver
21
+ @logger = logger || RubyLLM.logger
22
+ end
23
+
24
+ # @param url [String] A URL (e.g. https://news.ycombinator.com).
25
+ def execute(url:)
26
+ @logger.info("#{self.class.name}##{__method__} url=#{url.inspect}")
27
+
28
+ @driver.goto(url:)
29
+ end
30
+
31
+ private
32
+
33
+ def default_driver
34
+ if defined?(Watir)
35
+ WatirDriver.new(logger: @logger)
36
+ else
37
+ raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ # @example
7
+ # driver = SharedTools::Tools::Browser::WatirDriver.new
8
+ # driver.visit("https://example.com")
9
+ # driver.click(id: "submit-button")
10
+ class WatirDriver < BaseDriver
11
+ def initialize(logger: Logger.new(IO::NULL), browser: Watir::Browser.new(:chrome))
12
+ super(logger:)
13
+ @browser = browser
14
+ end
15
+
16
+ def close
17
+ @browser.close
18
+ end
19
+
20
+ # @return [String]
21
+ def url
22
+ @browser.url
23
+ end
24
+
25
+ # @return [String]
26
+ def title
27
+ @browser.title
28
+ end
29
+
30
+ # @return [String]
31
+ def html
32
+ @browser.html
33
+ end
34
+
35
+ # @param url [String]
36
+ def goto(url:)
37
+ @browser.goto(url)
38
+
39
+ { status: :ok }
40
+ end
41
+
42
+ # @yield [file]
43
+ # @yieldparam file [File]
44
+ def screenshot
45
+ tempfile = Tempfile.new(["screenshot", ".png"])
46
+ @browser.screenshot.save(tempfile.path)
47
+
48
+ yield File.open(tempfile.path, "rb")
49
+ ensure
50
+ tempfile&.close
51
+ tempfile&.unlink
52
+ end
53
+
54
+ # @param selector [String]
55
+ # @param text [String]
56
+ #
57
+ # @return [Hash]
58
+ def fill_in(selector:, text:)
59
+ element = find_field(selector)
60
+
61
+ return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
62
+
63
+ element.set(text)
64
+
65
+ { status: :ok }
66
+ end
67
+
68
+ # @param selector [String] e.g. "button[type='submit']", "div#parent > span.child", etc
69
+ #
70
+ # @return [Hash]
71
+ def click(selector:)
72
+ element = find_element(selector)
73
+
74
+ return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
75
+
76
+ element.click
77
+
78
+ { status: :ok }
79
+ end
80
+
81
+ protected
82
+
83
+ def wait_for_element(&)
84
+ Watir::Wait.until(timeout: TIMEOUT, &)
85
+ rescue Watir::Wait::TimeoutError
86
+ nil
87
+ end
88
+
89
+ # @param selector [String]
90
+ #
91
+ # @return [Watir::Input, Watir::TextArea, nil]
92
+ def find_field(selector)
93
+ wait_for_element { find_input_by(css: selector) || find_textarea_by(css: selector) }
94
+ end
95
+
96
+ # @param selector [Hash] A hash with one of the following
97
+ #
98
+ # @return [Watir::Element, nil]
99
+ def find_element(selector)
100
+ wait_for_element { find_element_by(css: selector) }
101
+ end
102
+
103
+ def find_element_by(selector)
104
+ element = @browser.element(selector)
105
+ return unless element.respond_to?(:exists?)
106
+
107
+ element if element.exists?
108
+ end
109
+
110
+ # @param selector [Hash]
111
+ #
112
+ # @return [Watir::TextArea, nil]
113
+ def find_textarea_by(selector)
114
+ element = @browser.textarea(selector)
115
+ return unless element
116
+
117
+ element if element.exists?
118
+ end
119
+
120
+ # @param selector [Hash]
121
+ #
122
+ # @return [Watir::Input, nil]
123
+ def find_input_by(selector)
124
+ element = @browser.input(selector)
125
+ return unless element
126
+
127
+ element if element.exists?
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection loader for all browser tools
4
+ # Usage: require 'shared_tools/tools/browser'
5
+
6
+ require 'shared_tools'
7
+
8
+ # Load base classes and utilities first (required by other components)
9
+ require_relative 'browser/base_driver'
10
+ require_relative 'browser/inspect_utils'
11
+
12
+ # Try to load watir for browser automation
13
+ begin
14
+ require 'watir'
15
+ require_relative 'browser/watir_driver'
16
+ rescue LoadError
17
+ # Watir gem not installed, BrowserTools will require manual driver
18
+ end
19
+
20
+ # Load tools (order matters - utils loaded first)
21
+ require_relative 'browser/visit_tool'
22
+ require_relative 'browser/click_tool'
23
+ require_relative 'browser/inspect_tool'
24
+ require_relative 'browser/page_inspect_tool'
25
+ require_relative 'browser/page_screenshot_tool'
26
+ require_relative 'browser/selector_inspect_tool'
27
+ require_relative 'browser/text_field_area_set_tool'