headless_browser_tool 0.1.1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.json +21 -0
  3. data/.rubocop.yml +56 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CLAUDE.md +298 -0
  7. data/LICENSE.md +7 -0
  8. data/README.md +522 -0
  9. data/Rakefile +12 -0
  10. data/config.ru +8 -0
  11. data/exe/hbt +7 -0
  12. data/lib/headless_browser_tool/browser.rb +374 -0
  13. data/lib/headless_browser_tool/browser_adapter.rb +320 -0
  14. data/lib/headless_browser_tool/cli.rb +34 -0
  15. data/lib/headless_browser_tool/directory_setup.rb +25 -0
  16. data/lib/headless_browser_tool/logger.rb +31 -0
  17. data/lib/headless_browser_tool/server.rb +150 -0
  18. data/lib/headless_browser_tool/session_manager.rb +199 -0
  19. data/lib/headless_browser_tool/session_middleware.rb +158 -0
  20. data/lib/headless_browser_tool/session_persistence.rb +146 -0
  21. data/lib/headless_browser_tool/stdio_server.rb +73 -0
  22. data/lib/headless_browser_tool/strict_session_middleware.rb +88 -0
  23. data/lib/headless_browser_tool/tools/attach_file_tool.rb +40 -0
  24. data/lib/headless_browser_tool/tools/auto_narrate_tool.rb +155 -0
  25. data/lib/headless_browser_tool/tools/base_tool.rb +39 -0
  26. data/lib/headless_browser_tool/tools/check_tool.rb +35 -0
  27. data/lib/headless_browser_tool/tools/choose_tool.rb +56 -0
  28. data/lib/headless_browser_tool/tools/click_button_tool.rb +49 -0
  29. data/lib/headless_browser_tool/tools/click_link_tool.rb +48 -0
  30. data/lib/headless_browser_tool/tools/click_tool.rb +45 -0
  31. data/lib/headless_browser_tool/tools/close_window_tool.rb +31 -0
  32. data/lib/headless_browser_tool/tools/double_click_tool.rb +37 -0
  33. data/lib/headless_browser_tool/tools/drag_tool.rb +46 -0
  34. data/lib/headless_browser_tool/tools/evaluate_script_tool.rb +20 -0
  35. data/lib/headless_browser_tool/tools/execute_script_tool.rb +29 -0
  36. data/lib/headless_browser_tool/tools/fill_in_tool.rb +66 -0
  37. data/lib/headless_browser_tool/tools/find_all_tool.rb +42 -0
  38. data/lib/headless_browser_tool/tools/find_element_tool.rb +21 -0
  39. data/lib/headless_browser_tool/tools/find_elements_containing_text_tool.rb +259 -0
  40. data/lib/headless_browser_tool/tools/get_attribute_tool.rb +21 -0
  41. data/lib/headless_browser_tool/tools/get_current_path_tool.rb +16 -0
  42. data/lib/headless_browser_tool/tools/get_current_url_tool.rb +16 -0
  43. data/lib/headless_browser_tool/tools/get_narration_history_tool.rb +35 -0
  44. data/lib/headless_browser_tool/tools/get_page_context_tool.rb +188 -0
  45. data/lib/headless_browser_tool/tools/get_page_source_tool.rb +16 -0
  46. data/lib/headless_browser_tool/tools/get_page_title_tool.rb +16 -0
  47. data/lib/headless_browser_tool/tools/get_session_info_tool.rb +37 -0
  48. data/lib/headless_browser_tool/tools/get_text_tool.rb +20 -0
  49. data/lib/headless_browser_tool/tools/get_value_tool.rb +20 -0
  50. data/lib/headless_browser_tool/tools/get_window_handles_tool.rb +29 -0
  51. data/lib/headless_browser_tool/tools/go_back_tool.rb +29 -0
  52. data/lib/headless_browser_tool/tools/go_forward_tool.rb +29 -0
  53. data/lib/headless_browser_tool/tools/has_element_tool.rb +21 -0
  54. data/lib/headless_browser_tool/tools/has_text_tool.rb +21 -0
  55. data/lib/headless_browser_tool/tools/hover_tool.rb +38 -0
  56. data/lib/headless_browser_tool/tools/is_visible_tool.rb +20 -0
  57. data/lib/headless_browser_tool/tools/maximize_window_tool.rb +34 -0
  58. data/lib/headless_browser_tool/tools/open_new_window_tool.rb +25 -0
  59. data/lib/headless_browser_tool/tools/refresh_tool.rb +32 -0
  60. data/lib/headless_browser_tool/tools/resize_window_tool.rb +43 -0
  61. data/lib/headless_browser_tool/tools/right_click_tool.rb +37 -0
  62. data/lib/headless_browser_tool/tools/save_page_tool.rb +32 -0
  63. data/lib/headless_browser_tool/tools/screenshot_tool.rb +199 -0
  64. data/lib/headless_browser_tool/tools/search_page_tool.rb +224 -0
  65. data/lib/headless_browser_tool/tools/search_source_tool.rb +148 -0
  66. data/lib/headless_browser_tool/tools/select_tool.rb +44 -0
  67. data/lib/headless_browser_tool/tools/switch_to_window_tool.rb +30 -0
  68. data/lib/headless_browser_tool/tools/uncheck_tool.rb +35 -0
  69. data/lib/headless_browser_tool/tools/visit_tool.rb +27 -0
  70. data/lib/headless_browser_tool/tools/visual_diff_tool.rb +177 -0
  71. data/lib/headless_browser_tool/tools.rb +104 -0
  72. data/lib/headless_browser_tool/version.rb +5 -0
  73. data/lib/headless_browser_tool.rb +8 -0
  74. metadata +256 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class ClickLinkTool < BaseTool
8
+ tool_name "click_link"
9
+ description "Click link by text or selector"
10
+
11
+ arguments do
12
+ required(:link_text_or_selector).filled(:string).description("Link text or CSS selector")
13
+ end
14
+
15
+ def execute(link_text_or_selector:)
16
+ url_before = browser.current_url
17
+ browser.title
18
+
19
+ # Find the link element first
20
+ link = begin
21
+ browser.find_link(link_text_or_selector)
22
+ rescue Capybara::ElementNotFound
23
+ browser.find(link_text_or_selector)
24
+ end
25
+
26
+ link_info = {
27
+ text: link.text.strip,
28
+ href: link[:href],
29
+ target: link[:target]
30
+ }.compact
31
+
32
+ browser.click_link(link_text_or_selector)
33
+
34
+ {
35
+ link: link_text_or_selector,
36
+ element: link_info,
37
+ navigation: {
38
+ navigated: browser.current_url != url_before,
39
+ from: url_before,
40
+ to: browser.current_url,
41
+ title: browser.title
42
+ },
43
+ status: "clicked"
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class ClickTool < BaseTool
8
+ tool_name "click"
9
+ description "Click an element by CSS selector"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element to click")
13
+ end
14
+
15
+ def execute(selector:)
16
+ # Capture state before click
17
+ url_before = browser.current_url
18
+
19
+ # Get element info before clicking
20
+ element = browser.find_element(selector)
21
+ element_text = element[:text]
22
+ tag_name = element[:tag_name]
23
+
24
+ # Perform click
25
+ browser.click(selector)
26
+
27
+ # Brief wait to allow page changes
28
+ sleep 0.1
29
+
30
+ {
31
+ selector: selector,
32
+ element: {
33
+ tag_name: tag_name,
34
+ text: element_text
35
+ },
36
+ navigation: {
37
+ url_before: url_before,
38
+ url_after: browser.current_url,
39
+ navigated: url_before != browser.current_url
40
+ }
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class CloseWindowTool < BaseTool
8
+ tool_name "close_window"
9
+ description "Close specific browser window/tab"
10
+
11
+ arguments do
12
+ required(:window_handle).filled(:string).description("Handle of the window to close")
13
+ end
14
+
15
+ def execute(window_handle:)
16
+ initial_windows = browser.windows
17
+ current_window = browser.current_window
18
+ browser.close_window(window_handle)
19
+
20
+ {
21
+ closed_window: window_handle,
22
+ was_current: window_handle == current_window,
23
+ previous_windows: initial_windows,
24
+ remaining_windows: browser.windows,
25
+ current_window: browser.current_window,
26
+ status: "closed"
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class DoubleClickTool < BaseTool
8
+ tool_name "double_click"
9
+ description "Double-click an element by CSS selector"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element to double-click")
13
+ end
14
+
15
+ def execute(selector:)
16
+ element = browser.find(selector)
17
+ element_info = {
18
+ tag_name: element.tag_name,
19
+ text: element.text.strip,
20
+ visible: element.visible?,
21
+ attributes: {
22
+ id: element[:id],
23
+ class: element[:class]
24
+ }.compact
25
+ }
26
+
27
+ browser.double_click(selector)
28
+
29
+ {
30
+ selector: selector,
31
+ element: element_info,
32
+ status: "double_clicked"
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class DragTool < BaseTool
8
+ tool_name "drag"
9
+ description "Drag an element to another element"
10
+
11
+ arguments do
12
+ required(:source_selector).filled(:string).description("CSS selector of the element to drag")
13
+ required(:target_selector).filled(:string).description("CSS selector of the target element")
14
+ end
15
+
16
+ def execute(source_selector:, target_selector:)
17
+ source = browser.find(source_selector)
18
+ target = browser.find(target_selector)
19
+
20
+ source_info = {
21
+ tag_name: source.tag_name,
22
+ text: source.text.strip,
23
+ id: source[:id],
24
+ class: source[:class]
25
+ }.compact
26
+
27
+ target_info = {
28
+ tag_name: target.tag_name,
29
+ text: target.text.strip,
30
+ id: target[:id],
31
+ class: target[:class]
32
+ }.compact
33
+
34
+ browser.drag(source_selector, target_selector)
35
+
36
+ {
37
+ source_selector: source_selector,
38
+ target_selector: target_selector,
39
+ source: source_info,
40
+ target: target_info,
41
+ status: "dragged"
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class EvaluateScriptTool < BaseTool
8
+ tool_name "evaluate_script"
9
+ description "Run JavaScript and return the result"
10
+
11
+ arguments do
12
+ required(:javascript_code).filled(:string).description("JavaScript code to evaluate")
13
+ end
14
+
15
+ def execute(javascript_code:)
16
+ browser.evaluate_script(javascript_code)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class ExecuteScriptTool < BaseTool
8
+ tool_name "execute_script"
9
+ description "Run JavaScript without return value"
10
+
11
+ arguments do
12
+ required(:javascript_code).filled(:string).description("JavaScript code to execute")
13
+ end
14
+
15
+ def execute(javascript_code:)
16
+ start_time = Time.now
17
+ browser.execute_script(javascript_code)
18
+ execution_time = Time.now - start_time
19
+
20
+ {
21
+ javascript_code: javascript_code,
22
+ execution_time: execution_time,
23
+ timestamp: Time.now.iso8601,
24
+ status: "executed"
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class FillInTool < BaseTool
8
+ tool_name "fill_in"
9
+ description "Fill in an input field"
10
+
11
+ arguments do
12
+ required(:field).filled(:string).description("Field name, id, or label")
13
+ required(:value).filled(:string).description("Value to fill in")
14
+ end
15
+
16
+ def execute(field:, value:)
17
+ # Try to find the field to get more info
18
+ field_info = begin
19
+ # Try different selectors
20
+ element = begin
21
+ browser.find_element("input[name='#{field}']")
22
+ rescue StandardError
23
+ nil
24
+ end
25
+ element ||= begin
26
+ browser.find_element("input##{field}")
27
+ rescue StandardError
28
+ nil
29
+ end
30
+ element ||= begin
31
+ browser.find_element("textarea[name='#{field}']")
32
+ rescue StandardError
33
+ nil
34
+ end
35
+ element ||= begin
36
+ browser.find_element("textarea##{field}")
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ if element
42
+ {
43
+ type: element[:attributes]["type"] || "text",
44
+ name: element[:attributes]["name"],
45
+ id: element[:attributes]["id"],
46
+ placeholder: element[:attributes]["placeholder"]
47
+ }.compact
48
+ else
49
+ {}
50
+ end
51
+ rescue StandardError
52
+ {}
53
+ end
54
+
55
+ browser.fill_in(field, value)
56
+
57
+ {
58
+ field: field,
59
+ value: value,
60
+ field_info: field_info,
61
+ status: "success"
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class FindAllTool < BaseTool
8
+ tool_name "find_all"
9
+ description "Find all matching elements, returns array"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector to find all matching elements")
13
+ end
14
+
15
+ def execute(selector:)
16
+ elements = browser.find_all(selector)
17
+
18
+ {
19
+ selector: selector,
20
+ count: elements.size,
21
+ elements: elements.map.with_index do |element, index|
22
+ result = {
23
+ index: index,
24
+ selector: "#{selector}:nth-of-type(#{index + 1})",
25
+ tag_name: element[:tag_name],
26
+ text: element[:text].strip,
27
+ visible: element[:visible]
28
+ }
29
+
30
+ # Include attributes if present
31
+ result[:attributes] = element[:attributes] unless element[:attributes].empty?
32
+
33
+ # Include value for form elements
34
+ result[:value] = element[:value] if element[:value] && !element[:value].empty?
35
+
36
+ result
37
+ end
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class FindElementTool < BaseTool
8
+ tool_name "find_element"
9
+ description "Find single element, errors if not found"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element to find")
13
+ end
14
+
15
+ def execute(selector:)
16
+ browser.find_element(selector)
17
+ "Found element: #{selector}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class FindElementsContainingTextTool < BaseTool
8
+ tool_name "find_elements_containing_text"
9
+ description "Find all elements containing specified text and return their selectors"
10
+
11
+ arguments do
12
+ required(:text).filled(:string).description("Text to search for in elements")
13
+ optional(:exact_match).filled(:bool).description("Require exact text match (default: false)")
14
+ optional(:case_sensitive).filled(:bool).description("Case sensitive search (default: false)")
15
+ optional(:visible_only).filled(:bool).description("Only return visible elements (default: true)")
16
+ end
17
+
18
+ def execute(text:, exact_match: false, case_sensitive: false, visible_only: true)
19
+ script = build_search_script(text, exact_match, case_sensitive, visible_only)
20
+ elements_data = browser.execute_script(script)
21
+
22
+ # Ensure we have an array to work with
23
+ elements_data = [] if elements_data.nil?
24
+ elements_data = [elements_data] unless elements_data.is_a?(Array)
25
+
26
+ # Process and enrich the results
27
+ results = elements_data.map do |element|
28
+ # Skip if element is not a hash
29
+ next unless element.is_a?(Hash)
30
+
31
+ # Safely extract values with defaults
32
+ {
33
+ tag: element["tag"] || "unknown",
34
+ text: (element["text"] || "").to_s,
35
+ selector: element["selector"] || "",
36
+ xpath: element["xpath"] || "",
37
+ attributes: element["attributes"] || {},
38
+ parent: element["parent"] || nil,
39
+ clickable: element["clickable"] == true,
40
+ visible: element["visible"] == true,
41
+ position: element["position"] || {}
42
+ }
43
+ end.compact
44
+
45
+ {
46
+ query: text,
47
+ total_found: results.size,
48
+ elements: results
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def build_search_script(text, exact_match, case_sensitive, visible_only)
55
+ <<~JS
56
+ (function() {
57
+ const searchText = #{text.to_json};
58
+ const exactMatch = #{exact_match};
59
+ const caseSensitive = #{case_sensitive};
60
+ const visibleOnly = #{visible_only};
61
+
62
+ // Helper to check if element is visible
63
+ function isVisible(elem) {
64
+ if (!elem) return false;
65
+ const style = window.getComputedStyle(elem);
66
+ return style.display !== 'none' &&
67
+ style.visibility !== 'hidden' &&
68
+ style.opacity !== '0' &&
69
+ elem.offsetWidth > 0 &&
70
+ elem.offsetHeight > 0;
71
+ }
72
+
73
+ // Helper to generate unique selector
74
+ function getSelector(elem) {
75
+ if (elem.id) {
76
+ return '#' + CSS.escape(elem.id);
77
+ }
78
+
79
+ if (elem.className && typeof elem.className === 'string') {
80
+ const classes = elem.className.trim().split(/\\s+/)
81
+ .filter(c => c.length > 0)
82
+ .map(c => '.' + CSS.escape(c))
83
+ .join('');
84
+ if (classes && document.querySelectorAll(elem.tagName + classes).length === 1) {
85
+ return elem.tagName.toLowerCase() + classes;
86
+ }
87
+ }
88
+
89
+ // Build path from root
90
+ const path = [];
91
+ let current = elem;
92
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
93
+ let selector = current.tagName.toLowerCase();
94
+ if (current.id) {
95
+ selector = '#' + CSS.escape(current.id);
96
+ path.unshift(selector);
97
+ break;
98
+ } else {
99
+ let sibling = current;
100
+ let nth = 1;
101
+ while (sibling.previousElementSibling) {
102
+ sibling = sibling.previousElementSibling;
103
+ if (sibling.tagName === current.tagName) nth++;
104
+ }
105
+ if (nth > 1) selector += ':nth-of-type(' + nth + ')';
106
+ }
107
+ path.unshift(selector);
108
+ current = current.parentElement;
109
+ }
110
+ return path.join(' > ');
111
+ }
112
+
113
+ // Helper to get XPath
114
+ function getXPath(elem) {
115
+ const path = [];
116
+ let current = elem;
117
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
118
+ let index = 0;
119
+ let sibling = current;
120
+ while (sibling) {
121
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
122
+ index++;
123
+ }
124
+ sibling = sibling.previousSibling;
125
+ if (sibling === current) break;
126
+ }
127
+ path.unshift(current.tagName.toLowerCase() + '[' + index + ']');
128
+ current = current.parentElement;
129
+ }
130
+ return '//' + path.join('/');
131
+ }
132
+
133
+ // Helper to check if text matches
134
+ function textMatches(elementText, searchText) {
135
+ if (!caseSensitive) {
136
+ elementText = elementText.toLowerCase();
137
+ searchText = searchText.toLowerCase();
138
+ }
139
+
140
+ if (exactMatch) {
141
+ return elementText.trim() === searchText.trim();
142
+ } else {
143
+ return elementText.includes(searchText);
144
+ }
145
+ }
146
+
147
+ // Helper to check if element is clickable
148
+ function isClickable(elem) {
149
+ const tag = elem.tagName.toLowerCase();
150
+ return tag === 'a' || tag === 'button' ||
151
+ elem.onclick !== null ||
152
+ elem.hasAttribute('onclick') ||
153
+ elem.style.cursor === 'pointer' ||
154
+ elem.role === 'button' ||
155
+ elem.role === 'link';
156
+ }
157
+
158
+ // Find all text nodes and their parent elements
159
+ const results = [];
160
+ const processed = new Set();
161
+ const walker = document.createTreeWalker(
162
+ document.body,
163
+ NodeFilter.SHOW_TEXT,
164
+ {
165
+ acceptNode: function(node) {
166
+ if (node.textContent.trim().length === 0) {
167
+ return NodeFilter.FILTER_REJECT;
168
+ }
169
+ return NodeFilter.FILTER_ACCEPT;
170
+ }
171
+ }
172
+ );
173
+
174
+ let node;
175
+ while (node = walker.nextNode()) {
176
+ const parent = node.parentElement;
177
+ if (!parent || processed.has(parent)) continue;
178
+
179
+ const text = parent.textContent;
180
+ if (textMatches(text, searchText)) {
181
+ if (visibleOnly && !isVisible(parent)) continue;
182
+
183
+ processed.add(parent);
184
+
185
+ // Get element attributes
186
+ const attributes = {};
187
+ if (parent.attributes && parent.attributes.length) {
188
+ for (let i = 0; i < parent.attributes.length; i++) {
189
+ const attr = parent.attributes[i];
190
+ attributes[attr.name] = attr.value;
191
+ }
192
+ }
193
+
194
+ // Get position
195
+ const rect = parent.getBoundingClientRect();
196
+
197
+ results.push({
198
+ tag: parent.tagName.toLowerCase(),
199
+ text: text.trim().substring(0, 200), // Limit text length
200
+ selector: getSelector(parent),
201
+ xpath: getXPath(parent),
202
+ attributes: attributes,
203
+ parent: parent.parentElement ? parent.parentElement.tagName.toLowerCase() : null,
204
+ clickable: isClickable(parent),
205
+ visible: isVisible(parent),
206
+ position: {
207
+ top: rect.top + window.scrollY,
208
+ left: rect.left + window.scrollX,
209
+ width: rect.width,
210
+ height: rect.height
211
+ }
212
+ });
213
+ }
214
+ }
215
+
216
+ // Also search in input values
217
+ document.querySelectorAll('input, textarea, select').forEach(elem => {
218
+ if (processed.has(elem)) return;
219
+
220
+ const value = elem.value || elem.textContent;
221
+ if (value && textMatches(value, searchText)) {
222
+ if (visibleOnly && !isVisible(elem)) return;
223
+
224
+ const attributes = {};
225
+ if (elem.attributes && elem.attributes.length) {
226
+ for (let i = 0; i < elem.attributes.length; i++) {
227
+ const attr = elem.attributes[i];
228
+ attributes[attr.name] = attr.value;
229
+ }
230
+ }
231
+
232
+ const rect = elem.getBoundingClientRect();
233
+
234
+ results.push({
235
+ tag: elem.tagName.toLowerCase(),
236
+ text: value.trim().substring(0, 200),
237
+ selector: getSelector(elem),
238
+ xpath: getXPath(elem),
239
+ attributes: attributes,
240
+ parent: elem.parentElement ? elem.parentElement.tagName.toLowerCase() : null,
241
+ clickable: true,
242
+ visible: isVisible(elem),
243
+ position: {
244
+ top: rect.top + window.scrollY,
245
+ left: rect.left + window.scrollX,
246
+ width: rect.width,
247
+ height: rect.height
248
+ }
249
+ });
250
+ }
251
+ });
252
+
253
+ return results;
254
+ })();
255
+ JS
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetAttributeTool < BaseTool
8
+ tool_name "get_attribute"
9
+ description "Get an attribute value from an element"
10
+
11
+ arguments do
12
+ required(:selector).filled(:string).description("CSS selector of the element")
13
+ required(:attribute_name).filled(:string).description("Name of the attribute to get")
14
+ end
15
+
16
+ def execute(selector:, attribute_name:)
17
+ browser.get_attribute(selector, attribute_name)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module HeadlessBrowserTool
6
+ module Tools
7
+ class GetCurrentPathTool < BaseTool
8
+ tool_name "get_current_path"
9
+ description "Get current path without domain"
10
+
11
+ def execute
12
+ browser.get_current_path
13
+ end
14
+ end
15
+ end
16
+ end