shared_tools 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -4
- data/README.md +257 -262
- data/lib/shared_tools/browser_tool.rb +5 -0
- data/lib/shared_tools/calculator_tool.rb +4 -0
- data/lib/shared_tools/clipboard_tool.rb +4 -0
- data/lib/shared_tools/composite_analysis_tool.rb +4 -0
- data/lib/shared_tools/computer_tool.rb +5 -0
- data/lib/shared_tools/cron_tool.rb +4 -0
- data/lib/shared_tools/current_date_time_tool.rb +4 -0
- data/lib/shared_tools/data_science_kit.rb +4 -0
- data/lib/shared_tools/database.rb +4 -0
- data/lib/shared_tools/database_query_tool.rb +4 -0
- data/lib/shared_tools/database_tool.rb +5 -0
- data/lib/shared_tools/disk_tool.rb +5 -0
- data/lib/shared_tools/dns_tool.rb +4 -0
- data/lib/shared_tools/doc_tool.rb +5 -0
- data/lib/shared_tools/error_handling_tool.rb +4 -0
- data/lib/shared_tools/eval_tool.rb +5 -0
- data/lib/shared_tools/mcp/brave_search_client.rb +37 -0
- data/lib/shared_tools/mcp/chart_client.rb +32 -0
- data/lib/shared_tools/mcp/github_client.rb +38 -0
- data/lib/shared_tools/mcp/hugging_face_client.rb +43 -0
- data/lib/shared_tools/mcp/memory_client.rb +33 -0
- data/lib/shared_tools/mcp/notion_client.rb +40 -0
- data/lib/shared_tools/mcp/sequential_thinking_client.rb +33 -0
- data/lib/shared_tools/mcp/slack_client.rb +54 -0
- data/lib/shared_tools/mcp/streamable_http_patch.rb +42 -0
- data/lib/shared_tools/mcp/tavily_client.rb +41 -0
- data/lib/shared_tools/mcp.rb +45 -16
- data/lib/shared_tools/system_info_tool.rb +4 -0
- data/lib/shared_tools/tools/browser/base_tool.rb +8 -12
- data/lib/shared_tools/tools/browser/click_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/ferrum_driver.rb +119 -0
- data/lib/shared_tools/tools/browser/inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +19 -7
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/visit_tool.rb +4 -2
- data/lib/shared_tools/tools/browser.rb +31 -2
- data/lib/shared_tools/tools/browser_tool.rb +14 -2
- data/lib/shared_tools/tools/clipboard_tool.rb +119 -0
- data/lib/shared_tools/tools/composite_analysis_tool.rb +60 -4
- data/lib/shared_tools/tools/computer/mac_driver.rb +37 -4
- data/lib/shared_tools/tools/computer_tool.rb +8 -2
- data/lib/shared_tools/tools/cron_tool.rb +332 -0
- data/lib/shared_tools/tools/current_date_time_tool.rb +88 -0
- data/lib/shared_tools/tools/data_science_kit.rb +63 -13
- data/lib/shared_tools/tools/database_tool.rb +8 -3
- data/lib/shared_tools/tools/dns_tool.rb +422 -0
- data/lib/shared_tools/tools/doc/docx_reader_tool.rb +107 -0
- data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +171 -0
- data/lib/shared_tools/tools/doc/text_reader_tool.rb +57 -0
- data/lib/shared_tools/tools/doc.rb +3 -0
- data/lib/shared_tools/tools/doc_tool.rb +101 -6
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +1 -1
- data/lib/shared_tools/tools/enabler.rb +42 -0
- data/lib/shared_tools/tools/error_handling_tool.rb +3 -1
- data/lib/shared_tools/tools/notification/base_driver.rb +51 -0
- data/lib/shared_tools/tools/notification/linux_driver.rb +115 -0
- data/lib/shared_tools/tools/notification/mac_driver.rb +66 -0
- data/lib/shared_tools/tools/notification/null_driver.rb +29 -0
- data/lib/shared_tools/tools/notification.rb +12 -0
- data/lib/shared_tools/tools/notification_tool.rb +99 -0
- data/lib/shared_tools/tools/system_info_tool.rb +204 -0
- data/lib/shared_tools/tools/workflow_manager_tool.rb +32 -0
- data/lib/shared_tools/utilities.rb +193 -0
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools/weather_tool.rb +4 -0
- data/lib/shared_tools/workflow_manager_tool.rb +4 -0
- data/lib/shared_tools.rb +42 -11
- metadata +79 -9
- data/lib/shared_tools/mcp/github_mcp_server.rb +0 -58
- data/lib/shared_tools/mcp/imcp.rb +0 -28
- data/lib/shared_tools/mcp/tavily_mcp_server.rb +0 -44
- data/lib/shared_tools/tools/devops_toolkit.rb +0 -420
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Browser
|
|
8
|
+
# A browser driver backed by Ferrum (Chrome DevTools Protocol).
|
|
9
|
+
# No chromedriver binary required — Ferrum talks directly to Chrome.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# driver = SharedTools::Tools::Browser::FerrumDriver.new
|
|
13
|
+
# driver.goto(url: "https://example.com")
|
|
14
|
+
# driver.click(selector: "a.some-link")
|
|
15
|
+
# driver.close
|
|
16
|
+
class FerrumDriver < BaseDriver
|
|
17
|
+
# @param logger [Logger]
|
|
18
|
+
# @param network_idle_timeout [Numeric] seconds to wait for network idle after navigation
|
|
19
|
+
# @param ferrum_options [Hash] additional options passed directly to Ferrum::Browser
|
|
20
|
+
def initialize(logger: Logger.new(IO::NULL), network_idle_timeout: 5, **ferrum_options)
|
|
21
|
+
super(logger:)
|
|
22
|
+
@network_idle_timeout = network_idle_timeout
|
|
23
|
+
options = {
|
|
24
|
+
headless: true,
|
|
25
|
+
timeout: TIMEOUT,
|
|
26
|
+
browser_options: { 'disable-blink-features' => 'AutomationControlled' }
|
|
27
|
+
}.merge(ferrum_options)
|
|
28
|
+
@browser = Ferrum::Browser.new(**options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def close
|
|
32
|
+
@browser.quit
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String]
|
|
36
|
+
def url
|
|
37
|
+
@browser.current_url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String]
|
|
41
|
+
def title
|
|
42
|
+
@browser.title
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String]
|
|
46
|
+
def html
|
|
47
|
+
@browser.evaluate('document.documentElement.outerHTML')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param url [String]
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def goto(url:)
|
|
53
|
+
@browser.go_to(url)
|
|
54
|
+
wait_for_network_idle
|
|
55
|
+
{ status: :ok }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @yield [file]
|
|
59
|
+
# @yieldparam file [File]
|
|
60
|
+
def screenshot
|
|
61
|
+
tempfile = Tempfile.new(['screenshot', '.png'])
|
|
62
|
+
@browser.screenshot(path: tempfile.path)
|
|
63
|
+
yield File.open(tempfile.path, 'rb')
|
|
64
|
+
ensure
|
|
65
|
+
tempfile&.close
|
|
66
|
+
tempfile&.unlink
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param selector [String] CSS selector for an input or textarea
|
|
70
|
+
# @param text [String]
|
|
71
|
+
# @return [Hash]
|
|
72
|
+
def fill_in(selector:, text:)
|
|
73
|
+
element = wait_for_element { @browser.at_css(selector) }
|
|
74
|
+
return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
|
|
75
|
+
|
|
76
|
+
element.evaluate("this.value = #{text.to_json}")
|
|
77
|
+
element.evaluate("this.dispatchEvent(new Event('input', {bubbles: true}))")
|
|
78
|
+
element.evaluate("this.dispatchEvent(new Event('change', {bubbles: true}))")
|
|
79
|
+
{ status: :ok }
|
|
80
|
+
rescue => e
|
|
81
|
+
{ status: :error, message: e.message }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param selector [String] CSS selector
|
|
85
|
+
# @return [Hash]
|
|
86
|
+
def click(selector:)
|
|
87
|
+
element = wait_for_element { @browser.at_css(selector) }
|
|
88
|
+
return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
|
|
89
|
+
|
|
90
|
+
element.click
|
|
91
|
+
{ status: :ok }
|
|
92
|
+
rescue => e
|
|
93
|
+
{ status: :error, message: e.message }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Wait until there are no pending network requests (JS-rendered pages).
|
|
99
|
+
# Falls back silently if Ferrum raises a timeout.
|
|
100
|
+
def wait_for_network_idle
|
|
101
|
+
@browser.network.wait_for_idle(duration: 0.3, timeout: @network_idle_timeout)
|
|
102
|
+
rescue Ferrum::TimeoutError
|
|
103
|
+
# Some requests never settle; accept whatever state the page is in.
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def wait_for_element
|
|
107
|
+
deadline = Time.now + TIMEOUT
|
|
108
|
+
loop do
|
|
109
|
+
result = yield
|
|
110
|
+
return result if result
|
|
111
|
+
break if Time.now >= deadline
|
|
112
|
+
sleep 0.2
|
|
113
|
+
end
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -44,10 +44,12 @@ module SharedTools
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
46
|
def default_driver
|
|
47
|
-
if defined?(
|
|
47
|
+
if defined?(Ferrum)
|
|
48
|
+
FerrumDriver.new(logger: @logger)
|
|
49
|
+
elsif defined?(Watir)
|
|
48
50
|
WatirDriver.new(logger: @logger)
|
|
49
51
|
else
|
|
50
|
-
raise LoadError, "Browser tools require a driver.
|
|
52
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -43,10 +43,12 @@ module SharedTools
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def default_driver
|
|
46
|
-
if defined?(
|
|
46
|
+
if defined?(Ferrum)
|
|
47
|
+
FerrumDriver.new(logger: @logger)
|
|
48
|
+
elsif defined?(Watir)
|
|
47
49
|
WatirDriver.new(logger: @logger)
|
|
48
50
|
else
|
|
49
|
-
raise LoadError, "Browser tools require a driver.
|
|
51
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
end
|
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
|
-
|
|
5
3
|
module SharedTools
|
|
6
4
|
module Tools
|
|
7
5
|
module Browser
|
|
8
6
|
# A browser automation tool for taking screenshots of the current page.
|
|
7
|
+
# Saves the screenshot to a file and returns the path — avoids injecting
|
|
8
|
+
# large base64 blobs into the conversation context.
|
|
9
9
|
class PageScreenshotTool < ::RubyLLM::Tool
|
|
10
10
|
def self.name = 'browser_page_screenshot'
|
|
11
11
|
|
|
12
|
-
description "
|
|
12
|
+
description "Take a screenshot of the current browser page and save it to a file."
|
|
13
|
+
|
|
14
|
+
params do
|
|
15
|
+
string :path, required: false,
|
|
16
|
+
description: "File path to save the screenshot (e.g. 'screenshot.png'). " \
|
|
17
|
+
"Defaults to a timestamped name in the current directory."
|
|
18
|
+
end
|
|
13
19
|
|
|
14
20
|
def initialize(driver: nil, logger: nil)
|
|
15
21
|
@driver = driver || default_driver
|
|
16
22
|
@logger = logger || RubyLLM.logger
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
def execute
|
|
25
|
+
def execute(path: nil)
|
|
20
26
|
@logger.info("#{self.class.name}##{__method__}")
|
|
21
27
|
|
|
28
|
+
save_path = path || "screenshot_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png"
|
|
29
|
+
|
|
22
30
|
@driver.screenshot do |file|
|
|
23
|
-
|
|
31
|
+
File.binwrite(save_path, file.read)
|
|
24
32
|
end
|
|
33
|
+
|
|
34
|
+
{ status: :ok, saved_to: File.expand_path(save_path) }
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
private
|
|
28
38
|
|
|
29
39
|
def default_driver
|
|
30
|
-
if defined?(
|
|
40
|
+
if defined?(Ferrum)
|
|
41
|
+
FerrumDriver.new(logger: @logger)
|
|
42
|
+
elsif defined?(Watir)
|
|
31
43
|
WatirDriver.new(logger: @logger)
|
|
32
44
|
else
|
|
33
|
-
raise LoadError, "Browser tools require a driver.
|
|
45
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
34
46
|
end
|
|
35
47
|
end
|
|
36
48
|
end
|
|
@@ -43,10 +43,12 @@ module SharedTools
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def default_driver
|
|
46
|
-
if defined?(
|
|
46
|
+
if defined?(Ferrum)
|
|
47
|
+
FerrumDriver.new(logger: @logger)
|
|
48
|
+
elsif defined?(Watir)
|
|
47
49
|
WatirDriver.new(logger: @logger)
|
|
48
50
|
else
|
|
49
|
-
raise LoadError, "Browser tools require a driver.
|
|
51
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
|
|
@@ -33,10 +33,12 @@ module SharedTools
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def default_driver
|
|
36
|
-
if defined?(
|
|
36
|
+
if defined?(Ferrum)
|
|
37
|
+
FerrumDriver.new(logger: @logger)
|
|
38
|
+
elsif defined?(Watir)
|
|
37
39
|
WatirDriver.new(logger: @logger)
|
|
38
40
|
else
|
|
39
|
-
raise LoadError, "Browser tools require a driver.
|
|
41
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
end
|
|
@@ -31,10 +31,12 @@ module SharedTools
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
33
|
def default_driver
|
|
34
|
-
if defined?(
|
|
34
|
+
if defined?(Ferrum)
|
|
35
|
+
FerrumDriver.new(logger: @logger)
|
|
36
|
+
elsif defined?(Watir)
|
|
35
37
|
WatirDriver.new(logger: @logger)
|
|
36
38
|
else
|
|
37
|
-
raise LoadError, "Browser tools require a driver.
|
|
39
|
+
raise LoadError, "Browser tools require a driver. Install the 'ferrum' gem or pass a driver: parameter"
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -9,12 +9,41 @@ require 'shared_tools'
|
|
|
9
9
|
require_relative 'browser/base_driver'
|
|
10
10
|
require_relative 'browser/inspect_utils'
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Load formatters (used by inspect_tool and related components)
|
|
13
|
+
require_relative 'browser/formatters/action_formatter'
|
|
14
|
+
require_relative 'browser/formatters/data_entry_formatter'
|
|
15
|
+
require_relative 'browser/formatters/element_formatter'
|
|
16
|
+
require_relative 'browser/formatters/input_formatter'
|
|
17
|
+
|
|
18
|
+
# Load elements helpers (used by inspect_tool)
|
|
19
|
+
require_relative 'browser/elements/element_grouper'
|
|
20
|
+
require_relative 'browser/elements/nearby_element_detector'
|
|
21
|
+
|
|
22
|
+
# Load page_inspect helpers (used by page_inspect_tool)
|
|
23
|
+
require_relative 'browser/page_inspect/button_summarizer'
|
|
24
|
+
require_relative 'browser/page_inspect/form_summarizer'
|
|
25
|
+
require_relative 'browser/page_inspect/html_summarizer'
|
|
26
|
+
require_relative 'browser/page_inspect/link_summarizer'
|
|
27
|
+
|
|
28
|
+
# Load selector_generator and its sub-modules (used by click_tool and related)
|
|
29
|
+
require_relative 'browser/selector_generator/base_selectors'
|
|
30
|
+
require_relative 'browser/selector_generator/contextual_selectors'
|
|
31
|
+
require_relative 'browser/selector_generator'
|
|
32
|
+
|
|
33
|
+
# Try to load Ferrum (preferred) for browser automation via Chrome DevTools Protocol
|
|
34
|
+
begin
|
|
35
|
+
require 'ferrum'
|
|
36
|
+
require_relative 'browser/ferrum_driver'
|
|
37
|
+
rescue LoadError
|
|
38
|
+
# Ferrum gem not installed
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fall back to Watir if available
|
|
13
42
|
begin
|
|
14
43
|
require 'watir'
|
|
15
44
|
require_relative 'browser/watir_driver'
|
|
16
45
|
rescue LoadError
|
|
17
|
-
# Watir gem not installed
|
|
46
|
+
# Watir gem not installed
|
|
18
47
|
end
|
|
19
48
|
|
|
20
49
|
# Load tools (order matters - utils loaded first)
|
|
@@ -146,10 +146,19 @@ module SharedTools
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
# @param logger [Logger] optional logger
|
|
149
|
-
# @param driver [SharedTools::Tools::Browser::BaseDriver] optional, will attempt to create WatirDriver
|
|
149
|
+
# @param driver [SharedTools::Tools::Browser::BaseDriver] optional, will attempt to create WatirDriver when execute is called
|
|
150
150
|
def initialize(logger: nil, driver: nil)
|
|
151
151
|
@logger = logger || RubyLLM.logger
|
|
152
|
-
@driver = driver
|
|
152
|
+
@driver = driver # Defer default_driver to execute time to support RubyLLM tool discovery
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Set driver after instantiation (useful when tool is discovered by RubyLLM)
|
|
156
|
+
attr_writer :driver
|
|
157
|
+
|
|
158
|
+
# Reports whether this tool can function.
|
|
159
|
+
# Returns true when a driver was injected or the watir gem is loaded.
|
|
160
|
+
def available?
|
|
161
|
+
!!@driver || defined?(Watir)
|
|
153
162
|
end
|
|
154
163
|
|
|
155
164
|
def cleanup!
|
|
@@ -166,6 +175,9 @@ module SharedTools
|
|
|
166
175
|
#
|
|
167
176
|
# @return [String]
|
|
168
177
|
def execute(action:, url: nil, selector: nil, value: nil, context_size: 2, full_html: false, text_content: nil)
|
|
178
|
+
# Lazily resolve driver at execute time
|
|
179
|
+
@driver ||= default_driver
|
|
180
|
+
|
|
169
181
|
case action.to_s.downcase
|
|
170
182
|
when Action::VISIT
|
|
171
183
|
require_param!(:url, url)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../shared_tools'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
# Read, write, and clear the system clipboard.
|
|
8
|
+
# Supports macOS (pbcopy/pbpaste), Linux (xclip or xsel), and Windows (clip/PowerShell).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# tool = SharedTools::Tools::ClipboardTool.new
|
|
12
|
+
# tool.execute(action: 'write', text: 'Hello!')
|
|
13
|
+
# tool.execute(action: 'read')
|
|
14
|
+
# tool.execute(action: 'clear')
|
|
15
|
+
class ClipboardTool < ::RubyLLM::Tool
|
|
16
|
+
def self.name = 'clipboard_tool'
|
|
17
|
+
|
|
18
|
+
description <<~DESC
|
|
19
|
+
Read from, write to, and clear the system clipboard.
|
|
20
|
+
Supports macOS, Linux (requires xclip or xsel), and Windows.
|
|
21
|
+
|
|
22
|
+
Actions:
|
|
23
|
+
- 'read' — Return the current clipboard contents
|
|
24
|
+
- 'write' — Replace clipboard contents with the given text
|
|
25
|
+
- 'clear' — Empty the clipboard
|
|
26
|
+
DESC
|
|
27
|
+
|
|
28
|
+
params do
|
|
29
|
+
string :action, description: "Action to perform: 'read', 'write', or 'clear'"
|
|
30
|
+
string :text, required: false, description: "Text to write. Required only for the 'write' action."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param logger [Logger] optional logger
|
|
34
|
+
def initialize(logger: nil)
|
|
35
|
+
@logger = logger || RubyLLM.logger
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param action [String] 'read', 'write', or 'clear'
|
|
39
|
+
# @param text [String, nil] text for write action
|
|
40
|
+
# @return [Hash] result
|
|
41
|
+
def execute(action:, text: nil)
|
|
42
|
+
@logger.info("ClipboardTool#execute action=#{action}")
|
|
43
|
+
|
|
44
|
+
case action.to_s.downcase
|
|
45
|
+
when 'read' then read_clipboard
|
|
46
|
+
when 'write' then write_clipboard(text)
|
|
47
|
+
when 'clear' then clear_clipboard
|
|
48
|
+
else
|
|
49
|
+
{ success: false, error: "Unknown action '#{action}'. Use: read, write, clear" }
|
|
50
|
+
end
|
|
51
|
+
rescue => e
|
|
52
|
+
@logger.error("ClipboardTool error: #{e.message}")
|
|
53
|
+
{ success: false, error: e.message }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def read_clipboard
|
|
59
|
+
content = clipboard_read
|
|
60
|
+
{ success: true, content: content, length: content.length }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_clipboard(text)
|
|
64
|
+
raise ArgumentError, "text is required for the write action" if text.nil?
|
|
65
|
+
clipboard_write(text)
|
|
66
|
+
{ success: true, message: "Text written to clipboard", length: text.length }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def clear_clipboard
|
|
70
|
+
clipboard_write('')
|
|
71
|
+
{ success: true, message: "Clipboard cleared" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clipboard_read
|
|
75
|
+
if macos?
|
|
76
|
+
`pbpaste`
|
|
77
|
+
elsif linux_xclip?
|
|
78
|
+
`xclip -selection clipboard -o 2>/dev/null`
|
|
79
|
+
elsif linux_xsel?
|
|
80
|
+
`xsel --clipboard --output 2>/dev/null`
|
|
81
|
+
elsif windows?
|
|
82
|
+
`powershell -command "Get-Clipboard" 2>/dev/null`.strip
|
|
83
|
+
else
|
|
84
|
+
raise "Clipboard not supported on this platform. Install xclip or xsel on Linux."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def clipboard_write(text)
|
|
89
|
+
if macos?
|
|
90
|
+
IO.popen('pbcopy', 'w') { |io| io.write(text) }
|
|
91
|
+
elsif linux_xclip?
|
|
92
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) }
|
|
93
|
+
elsif linux_xsel?
|
|
94
|
+
IO.popen('xsel --clipboard --input', 'w') { |io| io.write(text) }
|
|
95
|
+
elsif windows?
|
|
96
|
+
IO.popen('clip', 'w') { |io| io.write(text) }
|
|
97
|
+
else
|
|
98
|
+
raise "Clipboard not supported on this platform. Install xclip or xsel on Linux."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def macos?
|
|
103
|
+
RUBY_PLATFORM.include?('darwin')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def windows?
|
|
107
|
+
RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def linux_xclip?
|
|
111
|
+
@linux_xclip ||= system('which xclip > /dev/null 2>&1')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def linux_xsel?
|
|
115
|
+
@linux_xsel ||= system('which xsel > /dev/null 2>&1')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -18,12 +18,22 @@ module SharedTools
|
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
20
|
params do
|
|
21
|
-
string :data_source, description: <<~DESC.strip, required:
|
|
21
|
+
string :data_source, description: <<~DESC.strip, required: false
|
|
22
22
|
Primary data source to analyze. Can be either a local file path or a web URL.
|
|
23
23
|
For files: Use relative or absolute paths to CSV, JSON, XML, or text files.
|
|
24
24
|
For URLs: Use complete HTTP/HTTPS URLs to accessible data endpoints or web pages.
|
|
25
25
|
The tool automatically detects the source type and uses appropriate fetching methods.
|
|
26
26
|
Examples: './data/sales.csv', '/home/user/data.json', 'https://api.example.com/data'
|
|
27
|
+
Either data_source or data must be provided.
|
|
28
|
+
DESC
|
|
29
|
+
|
|
30
|
+
string :data, description: <<~DESC.strip, required: false
|
|
31
|
+
Inline data to analyze, provided directly as a string. Accepts:
|
|
32
|
+
- Pipe-delimited markdown tables (header row with | separators)
|
|
33
|
+
- CSV text (comma-separated rows, first row treated as headers)
|
|
34
|
+
- JSON text (object or array)
|
|
35
|
+
- Plain text (one item per line)
|
|
36
|
+
Either data or data_source must be provided.
|
|
27
37
|
DESC
|
|
28
38
|
|
|
29
39
|
string :analysis_type, description: <<~DESC.strip, required: false
|
|
@@ -48,16 +58,23 @@ module SharedTools
|
|
|
48
58
|
@logger = logger || RubyLLM.logger
|
|
49
59
|
end
|
|
50
60
|
|
|
51
|
-
def execute(data_source
|
|
61
|
+
def execute(data_source: nil, data: nil, analysis_type: "standard", **options)
|
|
62
|
+
if data_source.nil? && data.nil?
|
|
63
|
+
return { success: false, error: "Either data_source or data must be provided." }
|
|
64
|
+
end
|
|
65
|
+
|
|
52
66
|
results = {}
|
|
53
67
|
analysis_start = Time.now
|
|
54
68
|
|
|
55
69
|
begin
|
|
56
|
-
@logger.info("CompositeAnalysisTool#execute data_source=#{data_source} analysis_type=#{analysis_type}")
|
|
70
|
+
@logger.info("CompositeAnalysisTool#execute data_source=#{data_source.inspect} analysis_type=#{analysis_type}")
|
|
57
71
|
|
|
58
72
|
# Step 1: Fetch data using appropriate method
|
|
59
73
|
@logger.debug("Fetching data from source...")
|
|
60
|
-
if
|
|
74
|
+
if data
|
|
75
|
+
results[:data] = parse_inline_data(data)
|
|
76
|
+
results[:source_type] = 'inline'
|
|
77
|
+
elsif data_source.start_with?('http://', 'https://')
|
|
61
78
|
results[:data] = fetch_web_data(data_source)
|
|
62
79
|
results[:source_type] = 'web'
|
|
63
80
|
else
|
|
@@ -116,6 +133,45 @@ module SharedTools
|
|
|
116
133
|
|
|
117
134
|
private
|
|
118
135
|
|
|
136
|
+
# Parse inline data string into a Ruby structure.
|
|
137
|
+
# Handles pipe-delimited markdown tables, CSV, JSON, and plain text.
|
|
138
|
+
def parse_inline_data(raw)
|
|
139
|
+
raw = raw.strip
|
|
140
|
+
|
|
141
|
+
# JSON
|
|
142
|
+
if raw.start_with?('{', '[')
|
|
143
|
+
begin
|
|
144
|
+
return JSON.parse(raw)
|
|
145
|
+
rescue JSON::ParserError
|
|
146
|
+
# fall through
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
lines = raw.lines.map(&:strip).reject(&:empty?)
|
|
151
|
+
|
|
152
|
+
# Pipe-delimited markdown table: "Col A | Col B | Col C"
|
|
153
|
+
if lines.first&.include?('|')
|
|
154
|
+
headers = lines.first.split('|').map(&:strip).reject(&:empty?)
|
|
155
|
+
data_lines = lines.drop(1).reject { |l| l.match?(/^\|?[-:\s|]+$/) }
|
|
156
|
+
return data_lines.map do |line|
|
|
157
|
+
values = line.split('|').map(&:strip).reject(&:empty?)
|
|
158
|
+
headers.zip(values).to_h
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# CSV (comma-separated)
|
|
163
|
+
if lines.first&.include?(',')
|
|
164
|
+
headers = lines.first.split(',').map(&:strip)
|
|
165
|
+
return lines.drop(1).map do |line|
|
|
166
|
+
values = line.split(',').map(&:strip)
|
|
167
|
+
headers.zip(values).to_h
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Plain text — return as array of lines
|
|
172
|
+
lines
|
|
173
|
+
end
|
|
174
|
+
|
|
119
175
|
# Fetch data from web URL
|
|
120
176
|
def fetch_web_data(url)
|
|
121
177
|
@logger.debug("Fetching web data from: #{url}")
|
|
@@ -21,7 +21,15 @@ module SharedTools
|
|
|
21
21
|
# @param text [String]
|
|
22
22
|
# @param duration [Integer]
|
|
23
23
|
def hold_key(text:, duration:)
|
|
24
|
-
|
|
24
|
+
options = text.to_s.split('+')
|
|
25
|
+
key = options.pop
|
|
26
|
+
mask = options.reduce(0) { |m, opt| m | Library::CoreGraphics::EventFlags.find(opt) }
|
|
27
|
+
|
|
28
|
+
@keyboard.key_down(key, mask: mask)
|
|
29
|
+
Kernel.sleep(duration.to_f)
|
|
30
|
+
@keyboard.key_up(key, mask: mask)
|
|
31
|
+
|
|
32
|
+
{ success: true, key: text, duration: duration }
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
# @return [Hash<{ x: Integer, y: Integer }>]
|
|
@@ -86,10 +94,35 @@ module SharedTools
|
|
|
86
94
|
@keyboard.type(text)
|
|
87
95
|
end
|
|
88
96
|
|
|
89
|
-
# @param amount [Integer]
|
|
90
|
-
# @param direction [String]
|
|
97
|
+
# @param amount [Integer] number of scroll units
|
|
98
|
+
# @param direction [String] "up", "down", "left", or "right"
|
|
91
99
|
def scroll(amount:, direction:)
|
|
92
|
-
|
|
100
|
+
# Attach CGEventCreateScrollWheelEvent2 if not already done
|
|
101
|
+
unless Library::CoreGraphics.respond_to?(:CGEventCreateScrollWheelEvent2)
|
|
102
|
+
Library::CoreGraphics.module_eval do
|
|
103
|
+
attach_function :CGEventCreateScrollWheelEvent2,
|
|
104
|
+
[:pointer, :uint32, :uint32, :int32, :int32, :int32],
|
|
105
|
+
:pointer
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
amt = amount.to_i
|
|
110
|
+
# kCGScrollEventUnitLine = 1; wheel_count = 2 (vertical + horizontal)
|
|
111
|
+
delta_y, delta_x = case direction.to_s.downcase
|
|
112
|
+
when 'up' then [ amt, 0]
|
|
113
|
+
when 'down' then [-amt, 0]
|
|
114
|
+
when 'left' then [0, -amt]
|
|
115
|
+
when 'right' then [0, amt]
|
|
116
|
+
else [0, 0]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
event = Library::CoreGraphics.CGEventCreateScrollWheelEvent2(nil, 1, 2, delta_y, delta_x, 0)
|
|
120
|
+
Library::CoreGraphics.CGEventPost(
|
|
121
|
+
Library::CoreGraphics::EventTapLocation::HID_EVENT_TAP, event
|
|
122
|
+
)
|
|
123
|
+
Library::CoreGraphics.CFRelease(event)
|
|
124
|
+
|
|
125
|
+
{ success: true, direction: direction, amount: amt }
|
|
93
126
|
end
|
|
94
127
|
|
|
95
128
|
# @yield [file]
|
|
@@ -139,13 +139,16 @@ module SharedTools
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
# @param driver [Computer::BaseDriver] optional, will attempt to create platform-specific driver
|
|
142
|
+
# @param driver [Computer::BaseDriver] optional, will attempt to create platform-specific driver when execute is called
|
|
143
143
|
# @param logger [Logger] optional logger
|
|
144
144
|
def initialize(driver: nil, logger: nil)
|
|
145
145
|
@logger = logger || RubyLLM.logger
|
|
146
|
-
@driver = driver
|
|
146
|
+
@driver = driver # Defer default_driver to execute time to support RubyLLM tool discovery
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
# Set driver after instantiation (useful when tool is discovered by RubyLLM)
|
|
150
|
+
attr_writer :driver
|
|
151
|
+
|
|
149
152
|
# @param action [String]
|
|
150
153
|
# @param coordinate [Hash<{ width: Integer, height: Integer }>] the (x,y) coordinate
|
|
151
154
|
# @param text [String]
|
|
@@ -162,6 +165,9 @@ module SharedTools
|
|
|
162
165
|
scroll_direction: nil,
|
|
163
166
|
scroll_amount: nil
|
|
164
167
|
)
|
|
168
|
+
# Lazily resolve driver at execute time
|
|
169
|
+
@driver ||= default_driver
|
|
170
|
+
|
|
165
171
|
@logger.info({
|
|
166
172
|
action:,
|
|
167
173
|
coordinate:,
|