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
@@ -1,5 +1,22 @@
1
- # shared_tools/ruby_llm/mcp/github_mcp_server.rb
2
- # brew install github_mcp_server
1
+ # shared_tools/mcp/github_mcp_server.rb
2
+ #
3
+ # GitHub MCP Server Client Configuration for ruby_llm-mcp >= 0.7.0
4
+ #
5
+ # Provides access to GitHub operations including:
6
+ # - Repository management
7
+ # - Issue and PR operations
8
+ # - Code search
9
+ # - Branch operations
10
+ # - And more GitHub API functionality
11
+ #
12
+ # Installation:
13
+ # brew install github-mcp-server
14
+ #
15
+ # Configuration:
16
+ # Set environment variable: GITHUB_PERSONAL_ACCESS_TOKEN
17
+ # export GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here
18
+ #
19
+ # Compatible with ruby_llm-mcp v0.7.0+
3
20
 
4
21
  require "ruby_llm/mcp"
5
22
 
@@ -7,7 +24,7 @@ RubyLLM::MCP.add_client(
7
24
  name: "github-mcp-server",
8
25
  transport_type: :stdio,
9
26
  config: {
10
- command: "/opt/homebrew/bin/github-mcp-server", # brew install github-mcp-server
27
+ command: "/opt/homebrew/bin/github-mcp-server",
11
28
  args: %w[stdio],
12
29
  env: { "GITHUB_PERSONAL_ACCESS_TOKEN" => ENV.fetch("GITHUB_PERSONAL_ACCESS_TOKEN") },
13
30
  },
@@ -0,0 +1,28 @@
1
+ # shared_tools/mcp/imcp.rb
2
+ #
3
+ # iMCP Client Configuration for ruby_llm-mcp >= 0.7.0
4
+ #
5
+ # iMCP is a macOS application that provides MCP access to:
6
+ # - Notes
7
+ # - Calendar
8
+ # - Contacts
9
+ # - Reminders
10
+ # - And other macOS native applications
11
+ #
12
+ # Installation:
13
+ # brew install --cask loopwork/tap/iMCP
14
+ #
15
+ # Documentation:
16
+ # https://github.com/loopwork/iMCP
17
+ #
18
+ # Compatible with ruby_llm-mcp v0.7.0+
19
+
20
+ require 'ruby_llm/mcp'
21
+
22
+ RubyLLM::MCP.add_client(
23
+ name: "imcp-server",
24
+ transport_type: :stdio,
25
+ config: {
26
+ command: "/Applications/iMCP.app/Contents/MacOS/imcp-server 2> /dev/null"
27
+ }
28
+ )
@@ -0,0 +1,44 @@
1
+ # shared_tools/mcp/tavily_mcp_server.rb
2
+ #
3
+ # Tavily MCP Server Client Configuration for ruby_llm-mcp >= 0.7.0
4
+ #
5
+ # Provides AI-powered web search and research capabilities:
6
+ # - Web search with AI-optimized results
7
+ # - Research-grade content extraction
8
+ # - Real-time information gathering
9
+ # - News and current events search
10
+ #
11
+ # Installation:
12
+ # Requires Node.js and npx (comes with Node.js)
13
+ # The mcp-remote package will be installed automatically via npx
14
+ #
15
+ # Configuration:
16
+ # Set environment variable: TAVILY_API_KEY
17
+ # export TAVILY_API_KEY=your_api_key_here
18
+ # Get your API key at: https://tavily.com
19
+ #
20
+ # Compatible with ruby_llm-mcp v0.7.0+
21
+
22
+ require "ruby_llm/mcp"
23
+
24
+ RubyLLM::MCP.add_client(
25
+ name: "tavily",
26
+ transport_type: :stdio,
27
+ config: {
28
+ command: "npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=#{ENV.fetch('TAVILY_API_KEY')}",
29
+ env: {}
30
+ }
31
+ )
32
+
33
+
34
+ __END__
35
+
36
+
37
+ # {
38
+ # "mcpServers": {
39
+ # "tavily-remote-mcp": {
40
+ # "command": "npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=$TAVILY_API_KEY",
41
+ # "env": {}
42
+ # }
43
+ # }
44
+ # }
@@ -0,0 +1,24 @@
1
+ # lib/shared_tools/mcp.rb
2
+ #
3
+ # MCP (Model Context Protocol) support for SharedTools using ruby_llm-mcp gem >= 0.7.0
4
+ #
5
+ # This module provides integration with various MCP servers, allowing Ruby applications
6
+ # to connect to external services and tools through the Model Context Protocol.
7
+ #
8
+ # @see https://github.com/patvice/ruby_llm-mcp RubyLLM MCP documentation
9
+ # @see https://www.rubyllm-mcp.com Official documentation
10
+ #
11
+ # Usage:
12
+ # require 'shared_tools/mcp/imcp' # Load iMCP client
13
+ # require 'shared_tools/mcp/github_mcp_server' # Load GitHub client
14
+ # require 'shared_tools/mcp/tavily_mcp_server' # Load Tavily client
15
+ #
16
+ # Requirements:
17
+ # - ruby_llm-mcp >= 0.7.0
18
+ # - RubyLLM >= 1.9.0
19
+ #
20
+ # Version 0.7.0 Changes:
21
+ # - Complex parameter support is now enabled by default
22
+ # - Requires RubyLLM 1.9+
23
+ # - support_complex_parameters! method is deprecated
24
+ #
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ # A base driver intended to be overridden by specific browser drivers (e.g. waitir).
7
+ class BaseDriver
8
+ TIMEOUT = Integer(ENV.fetch("OMNIAI_BROWSER_TIMEOUT", 10))
9
+
10
+ # @param logger [Logger]
11
+ def initialize(logger: Logger.new(IO::NULL))
12
+ @logger = logger
13
+ end
14
+
15
+ def close
16
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
17
+ end
18
+
19
+ # @return [String]
20
+ def url
21
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
22
+ end
23
+
24
+ # @return [String]
25
+ def title
26
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
27
+ end
28
+
29
+ # @return [String]
30
+ def html
31
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
32
+ end
33
+
34
+ # @yield [file]
35
+ # @yieldparam file [File]
36
+ def screenshot
37
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
38
+ end
39
+
40
+ # @param url [String]
41
+ #
42
+ # @return [Hash]
43
+ def goto(url:)
44
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
45
+ end
46
+
47
+ # @param selector [String]
48
+ # @param text [String]
49
+ #
50
+ # @return [Hash]
51
+ def fill_in(selector:, text:)
52
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
53
+ end
54
+
55
+ # @param selector [String] e.g. "button[type='submit']", "div#parent > span.child", etc
56
+ #
57
+ # @return [Hash]
58
+ def click(selector:)
59
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "watir"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Browser
8
+ # @example
9
+ # class SeleniumTool < BaseTool
10
+ # # ...
11
+ # end
12
+ class BaseTool
13
+ # @param logger [Logger]
14
+ # @param driver [BaseDriver]
15
+ def initialize(driver:, logger: Logger.new(IO::NULL))
16
+ super()
17
+ @driver = driver
18
+ @logger = logger
19
+ end
20
+
21
+ protected
22
+
23
+ def wait_for_element
24
+ return yield if defined?(RSpec) # Skip waiting in tests
25
+
26
+ Watir::Wait.until(timeout: 10) do
27
+ element = yield
28
+ element if element && element_visible?(element)
29
+ end
30
+ rescue Watir::Wait::TimeoutError
31
+ log_element_timeout
32
+ nil
33
+ end
34
+
35
+ def element_visible?(element)
36
+ return true unless element.respond_to?(:visible?)
37
+
38
+ element.visible?
39
+ end
40
+
41
+ def log_element_timeout
42
+ return unless @browser.respond_to?(:elements)
43
+
44
+ visible_elements = @browser.elements.select(&:visible?).map(&:text).compact.first(10)
45
+ @logger.error("Element not found after 10s. Sample visible elements: #{visible_elements}")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,54 @@
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::ClickTool.new(browser:)
9
+ # tool.execute(selector: "#some-id")
10
+ # tool.execute(selector: ".some-class")
11
+ # tool.execute(selector: "some text")
12
+ # tool.execute(selector: "//div[@role='button']")
13
+ class ClickTool < ::RubyLLM::Tool
14
+ def self.name = 'browser_click'
15
+
16
+ description "A browser automation tool for clicking any clickable element."
17
+
18
+ params do
19
+ string :selector, description: <<~TEXT.strip
20
+ A CSS selector to locate or interact with an element on the page:
21
+
22
+ * 'form button[type="submit"]': selects a button with type submit
23
+ * '.example': selects elements with the foo and bar classes
24
+ * '#example': selects an element by ID
25
+ * 'div#parent > span.child': selects span elements that are direct children of div elements
26
+ * 'a[href="/login"]': selects an anchor tag with a specific href attribute
27
+ TEXT
28
+ end
29
+
30
+ def initialize(driver: nil, logger: nil)
31
+ @driver = driver || default_driver
32
+ @logger = logger || RubyLLM.logger
33
+ end
34
+
35
+ # @param selector [String] CSS selector, ID, text content, or other identifier for the element to click.
36
+ def execute(selector:)
37
+ @logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
38
+
39
+ @driver.click(selector:)
40
+ end
41
+
42
+ private
43
+
44
+ def default_driver
45
+ if defined?(Watir)
46
+ WatirDriver.new(logger: @logger)
47
+ else
48
+ raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module Elements
7
+ # Groups HTML elements by their relevance for data entry
8
+ module ElementGrouper
9
+ module_function
10
+
11
+ def group_for_data_entry(elements, text)
12
+ groups = {
13
+ inputs: [], # text, number, email inputs - primary targets
14
+ form_controls: {}, # radio buttons, checkboxes grouped by name
15
+ labels: [], # labels, headers, spans
16
+ actions: [], # buttons and actionable elements
17
+ }
18
+
19
+ elements.each do |element|
20
+ categorize_element(element, groups, text)
21
+ end
22
+
23
+ groups
24
+ end
25
+
26
+ def categorize_element(element, groups, text)
27
+ case element.name.downcase
28
+ when "input"
29
+ categorize_input_element(element, groups)
30
+ when "textarea", "select"
31
+ groups[:inputs] << element
32
+ when "button"
33
+ groups[:actions] << element if contains_text_match?(element, text)
34
+ when "label", "span", "div", "th"
35
+ groups[:labels] << element if contains_text_match?(element, text)
36
+ end
37
+ end
38
+
39
+ def categorize_input_element(element, groups)
40
+ if data_entry_input?(element)
41
+ groups[:inputs] << element
42
+ else
43
+ group_form_control(element, groups[:form_controls])
44
+ end
45
+ end
46
+
47
+ def data_entry_input?(element)
48
+ # Handle missing type attribute - HTML default is "text"
49
+ type = (element["type"] || "text").downcase
50
+
51
+ # Include common data entry input types
52
+ %w[text number email tel url date datetime-local time month week
53
+ password search].include?(type)
54
+ end
55
+
56
+ def group_form_control(element, form_controls)
57
+ type = (element["type"] || "text").downcase
58
+ return unless %w[radio checkbox].include?(type)
59
+
60
+ name = element["name"] || "unnamed"
61
+ form_controls[name] ||= []
62
+ form_controls[name] << element
63
+ end
64
+
65
+ def contains_text_match?(element, text)
66
+ element.text.downcase.include?(text.downcase) ||
67
+ element["value"]&.downcase&.include?(text.downcase)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module Elements
7
+ # Handles detection of interactive elements near text matches
8
+ module NearbyElementDetector
9
+ module_function
10
+
11
+ def add_nearby_interactive_elements(elements)
12
+ nearby_elements = []
13
+ elements.each do |element|
14
+ nearby_elements += find_interactive_in_context(element)
15
+ end
16
+ combined = elements.to_a + nearby_elements
17
+ combined.uniq
18
+ end
19
+
20
+ def find_interactive_in_context(element)
21
+ interactive_elements = []
22
+ interactive_elements += check_table_context(element)
23
+ interactive_elements += check_parent_containers(element)
24
+ interactive_elements.uniq
25
+ end
26
+
27
+ def check_table_context(element)
28
+ elements = []
29
+ table_cell = element.ancestors("td").first || element.ancestors("th").first
30
+ elements += find_interactive_in_container(table_cell) if table_cell
31
+ table_row = element.ancestors("tr").first
32
+ elements += find_interactive_in_container(table_row) if table_row
33
+ if table_cell && table_row
34
+ table = element.ancestors("table").first
35
+ elements += find_interactive_in_same_column(table, table_cell, table_row) if table
36
+ end
37
+ elements
38
+ end
39
+
40
+ def find_interactive_in_same_column(table, header_cell, header_row)
41
+ return [] unless table
42
+
43
+ column_index = header_row.css("th, td").index(header_cell)
44
+ return [] unless column_index
45
+
46
+ interactive_elements = []
47
+ table.css("tbody tr").each do |row|
48
+ interactive_elements += find_column_cell_elements(row, column_index)
49
+ end
50
+ interactive_elements
51
+ end
52
+
53
+ def find_column_cell_elements(row, column_index)
54
+ target_cell = row.css("td, th")[column_index]
55
+ return [] unless target_cell
56
+
57
+ find_interactive_in_container(target_cell) + find_interactive_in_nested_tables(target_cell, column_index)
58
+ end
59
+
60
+ def find_interactive_in_nested_tables(cell, original_column_index)
61
+ interactive_elements = []
62
+
63
+ cell.css("table").each do |nested_table|
64
+ nested_table.css("tr").each do |nested_row|
65
+ nested_cells = nested_row.css("td, th")
66
+ if nested_cells[original_column_index]
67
+ interactive_elements += find_interactive_in_container(nested_cells[original_column_index])
68
+ end
69
+ end
70
+ end
71
+
72
+ interactive_elements
73
+ end
74
+
75
+ def check_parent_containers(element)
76
+ interactive_elements = []
77
+ parent_container = element.parent
78
+ 3.times do
79
+ break unless parent_container
80
+ break if parent_container.is_a?(Nokogiri::XML::Document)
81
+
82
+ interactive_elements += find_interactive_in_container(parent_container)
83
+ parent_container = parent_container.parent
84
+ end
85
+ interactive_elements
86
+ end
87
+
88
+ def find_interactive_in_container(container)
89
+ return [] unless container
90
+
91
+ container.css("input, textarea, select, button, [role='button'], [tabindex='0']")
92
+ .reject { |el| non_interactive_element?(el) }
93
+ end
94
+
95
+ def non_interactive_element?(element)
96
+ return true if element["type"] == "hidden"
97
+ return true if element["tabindex"] == "-1"
98
+ return true if element["aria-hidden"] == "true"
99
+
100
+ style = element["style"]
101
+ return true if style&.include?("display: none") || style&.include?("visibility: hidden")
102
+
103
+ false
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Handles formatting of action elements (buttons, links)
8
+ module ActionFormatter
9
+ module_function
10
+
11
+ def format_actions(actions)
12
+ result = "⚡ Available Actions:\n"
13
+ actions.first(5).each do |action|
14
+ result += format_action_element(action)
15
+ end
16
+ result += " ... and #{actions.size - 5} more\n" if actions.size > 5
17
+ result += "\n"
18
+ end
19
+
20
+ def format_action_element(action)
21
+ text = action.text.strip
22
+ selector = get_action_selector(action)
23
+ " • #{text} (#{selector})\n"
24
+ end
25
+
26
+ def get_action_selector(action)
27
+ return action["id"] if action["id"] && !action["id"].empty?
28
+ return "text:#{action.text.strip}" if action.text.strip.length > 2
29
+ return action["class"].split.first if action["class"]
30
+
31
+ "css-needed"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Formats grouped elements for optimal data entry display
8
+ module DataEntryFormatter
9
+ module_function
10
+
11
+ def format_groups(grouped_elements, total_count, text)
12
+ result = build_header(total_count, text)
13
+ result += format_each_group(grouped_elements)
14
+ result
15
+ end
16
+
17
+ def format_inputs(inputs)
18
+ result = "📝 Data Entry Fields:\n"
19
+ table_groups = group_by_table_context(inputs)
20
+ result += format_table_groups(table_groups)
21
+ "#{result}\n"
22
+ end
23
+
24
+ def format_form_controls(form_controls)
25
+ result = "🎛️ Form Controls:\n"
26
+ form_controls.each do |name, controls|
27
+ result += format_control_group(name, controls)
28
+ end
29
+ result += "\n"
30
+ end
31
+
32
+ def format_labels(labels)
33
+ result = "🏷️ Labels & Headers:\n"
34
+ labels.first(3).each do |label|
35
+ result += " • #{label.name}: #{label.text.strip}\n"
36
+ end
37
+ result += " ... and #{labels.size - 3} more\n" if labels.size > 3
38
+ result += "\n"
39
+ end
40
+
41
+ def build_header(total_count, text)
42
+ "Found #{total_count} elements containing '#{text}':\n\n"
43
+ end
44
+
45
+ def format_each_group(grouped_elements)
46
+ [
47
+ format_inputs_section(grouped_elements[:inputs]),
48
+ format_actions_section(grouped_elements[:actions]),
49
+ format_controls_section(grouped_elements[:form_controls]),
50
+ format_labels_section(grouped_elements[:labels]),
51
+ ].join
52
+ end
53
+
54
+ def format_inputs_section(inputs)
55
+ inputs.any? ? format_inputs(inputs) : ""
56
+ end
57
+
58
+ def format_actions_section(actions)
59
+ actions.any? ? ActionFormatter.format_actions(actions) : ""
60
+ end
61
+
62
+ def format_controls_section(form_controls)
63
+ form_controls.any? ? format_form_controls(form_controls) : ""
64
+ end
65
+
66
+ def format_labels_section(labels)
67
+ labels.any? ? format_labels(labels) : ""
68
+ end
69
+
70
+ def format_control_group(name, controls)
71
+ result = "\n #{name.humanize} options:\n"
72
+ controls.each do |control|
73
+ checked = control["checked"] ? " \u2713" : ""
74
+ result += " • #{control['value']}#{checked}\n"
75
+ end
76
+ result
77
+ end
78
+
79
+ def format_table_groups(table_groups)
80
+ return format_single_group(table_groups.values.first) if table_groups.size == 1
81
+
82
+ result = ""
83
+ table_groups.each do |(context, group_inputs)|
84
+ result += "\n #{context}:\n"
85
+ group_inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
86
+ end
87
+ result
88
+ end
89
+
90
+ def format_single_group(inputs)
91
+ result = ""
92
+ inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
93
+ result
94
+ end
95
+
96
+ def group_by_table_context(inputs)
97
+ groups = {}
98
+ inputs.each do |input|
99
+ context = find_context(input)
100
+ groups[context] ||= []
101
+ groups[context] << input
102
+ end
103
+ groups
104
+ end
105
+
106
+ def find_context(input)
107
+ table = input.ancestors("table").first
108
+ return "Form Fields" unless table
109
+
110
+ find_table_context(input)
111
+ end
112
+
113
+ def find_table_context(input)
114
+ row = input.ancestors("tr").first
115
+ return "Table" unless row
116
+
117
+ find_meaningful_cell_text(row)
118
+ end
119
+
120
+ def find_meaningful_cell_text(row)
121
+ meaningful_cell = row.css("td, th").find do |cell|
122
+ cell.text.strip.length > 2 && !cell.text.match?(/^\d+\.?\d*$/)
123
+ end
124
+
125
+ meaningful_cell ? meaningful_cell.text.strip[0..30] : "Table"
126
+ end
127
+
128
+ def humanize(text)
129
+ text.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end