shared_tools 0.3.1 → 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 -16
- 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 +6 -0
- data/lib/shared_tools/tools/clipboard_tool.rb +69 -144
- 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/cron_tool.rb +237 -379
- data/lib/shared_tools/tools/current_date_time_tool.rb +54 -120
- data/lib/shared_tools/tools/data_science_kit.rb +63 -13
- data/lib/shared_tools/tools/dns_tool.rb +335 -269
- 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 +130 -343
- 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 +28 -38
- metadata +74 -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)
|
|
@@ -155,6 +155,12 @@ module SharedTools
|
|
|
155
155
|
# Set driver after instantiation (useful when tool is discovered by RubyLLM)
|
|
156
156
|
attr_writer :driver
|
|
157
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)
|
|
162
|
+
end
|
|
163
|
+
|
|
158
164
|
def cleanup!
|
|
159
165
|
@driver.close
|
|
160
166
|
end
|
|
@@ -1,59 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative '../../shared_tools'
|
|
4
4
|
|
|
5
5
|
module SharedTools
|
|
6
6
|
module Tools
|
|
7
|
-
#
|
|
8
|
-
# Supports macOS (pbcopy/pbpaste), Linux (xclip
|
|
7
|
+
# Read, write, and clear the system clipboard.
|
|
8
|
+
# Supports macOS (pbcopy/pbpaste), Linux (xclip or xsel), and Windows (clip/PowerShell).
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
11
|
# tool = SharedTools::Tools::ClipboardTool.new
|
|
12
|
-
# tool.execute(action: 'write',
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
class ClipboardTool < RubyLLM::Tool
|
|
16
|
-
def self.name = '
|
|
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
17
|
|
|
18
|
-
description <<~
|
|
19
|
-
Read from
|
|
20
|
-
|
|
21
|
-
This tool provides cross-platform clipboard access:
|
|
22
|
-
- macOS: Uses pbcopy/pbpaste
|
|
23
|
-
- Linux: Uses xclip or xsel (must be installed)
|
|
24
|
-
- Windows: Uses clip/powershell
|
|
18
|
+
description <<~DESC
|
|
19
|
+
Read from, write to, and clear the system clipboard.
|
|
20
|
+
Supports macOS, Linux (requires xclip or xsel), and Windows.
|
|
25
21
|
|
|
26
22
|
Actions:
|
|
27
|
-
- 'read'
|
|
28
|
-
- 'write'
|
|
29
|
-
- 'clear'
|
|
30
|
-
|
|
31
|
-
Example usage:
|
|
32
|
-
tool = SharedTools::Tools::ClipboardTool.new
|
|
33
|
-
|
|
34
|
-
# Write to clipboard
|
|
35
|
-
tool.execute(action: 'write', content: 'Hello, World!')
|
|
36
|
-
|
|
37
|
-
# Read from clipboard
|
|
38
|
-
result = tool.execute(action: 'read')
|
|
39
|
-
puts result[:content]
|
|
40
|
-
|
|
41
|
-
# Clear clipboard
|
|
42
|
-
tool.execute(action: 'clear')
|
|
43
|
-
DESCRIPTION
|
|
23
|
+
- 'read' — Return the current clipboard contents
|
|
24
|
+
- 'write' — Replace clipboard contents with the given text
|
|
25
|
+
- 'clear' — Empty the clipboard
|
|
26
|
+
DESC
|
|
44
27
|
|
|
45
28
|
params do
|
|
46
|
-
string :action, description:
|
|
47
|
-
|
|
48
|
-
- 'read': Get current clipboard contents
|
|
49
|
-
- 'write': Set clipboard contents (requires 'content' parameter)
|
|
50
|
-
- 'clear': Clear the clipboard
|
|
51
|
-
DESC
|
|
52
|
-
|
|
53
|
-
string :content, description: <<~DESC.strip, required: false
|
|
54
|
-
The text content to write to the clipboard.
|
|
55
|
-
Required when action is 'write'.
|
|
56
|
-
DESC
|
|
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."
|
|
57
31
|
end
|
|
58
32
|
|
|
59
33
|
# @param logger [Logger] optional logger
|
|
@@ -61,133 +35,84 @@ module SharedTools
|
|
|
61
35
|
@logger = logger || RubyLLM.logger
|
|
62
36
|
end
|
|
63
37
|
|
|
64
|
-
# Execute clipboard action
|
|
65
|
-
#
|
|
66
38
|
# @param action [String] 'read', 'write', or 'clear'
|
|
67
|
-
# @param
|
|
68
|
-
# @return [Hash] result
|
|
69
|
-
def execute(action:,
|
|
70
|
-
@logger.info("ClipboardTool#execute action=#{action
|
|
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}")
|
|
71
43
|
|
|
72
44
|
case action.to_s.downcase
|
|
73
|
-
when 'read'
|
|
74
|
-
|
|
75
|
-
when '
|
|
76
|
-
write_clipboard(content)
|
|
77
|
-
when 'clear'
|
|
78
|
-
clear_clipboard
|
|
45
|
+
when 'read' then read_clipboard
|
|
46
|
+
when 'write' then write_clipboard(text)
|
|
47
|
+
when 'clear' then clear_clipboard
|
|
79
48
|
else
|
|
80
|
-
{
|
|
81
|
-
success: false,
|
|
82
|
-
error: "Unknown action: #{action}. Valid actions are: read, write, clear"
|
|
83
|
-
}
|
|
49
|
+
{ success: false, error: "Unknown action '#{action}'. Use: read, write, clear" }
|
|
84
50
|
end
|
|
85
51
|
rescue => e
|
|
86
52
|
@logger.error("ClipboardTool error: #{e.message}")
|
|
87
|
-
{
|
|
88
|
-
success: false,
|
|
89
|
-
error: e.message
|
|
90
|
-
}
|
|
53
|
+
{ success: false, error: e.message }
|
|
91
54
|
end
|
|
92
55
|
|
|
93
56
|
private
|
|
94
57
|
|
|
95
58
|
def read_clipboard
|
|
96
|
-
content =
|
|
97
|
-
|
|
98
|
-
`pbpaste 2>/dev/null`
|
|
99
|
-
when :linux
|
|
100
|
-
if command_exists?('xclip')
|
|
101
|
-
`xclip -selection clipboard -o 2>/dev/null`
|
|
102
|
-
elsif command_exists?('xsel')
|
|
103
|
-
`xsel --clipboard --output 2>/dev/null`
|
|
104
|
-
else
|
|
105
|
-
raise "No clipboard tool found. Install xclip or xsel."
|
|
106
|
-
end
|
|
107
|
-
when :windows
|
|
108
|
-
`powershell -command "Get-Clipboard" 2>nul`.chomp
|
|
109
|
-
else
|
|
110
|
-
raise "Unsupported platform: #{RUBY_PLATFORM}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
success: true,
|
|
115
|
-
content: content,
|
|
116
|
-
length: content.length
|
|
117
|
-
}
|
|
59
|
+
content = clipboard_read
|
|
60
|
+
{ success: true, content: content, length: content.length }
|
|
118
61
|
end
|
|
119
62
|
|
|
120
|
-
def write_clipboard(
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
end
|
|
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
|
|
127
68
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
83
|
else
|
|
142
|
-
raise "
|
|
84
|
+
raise "Clipboard not supported on this platform. Install xclip or xsel on Linux."
|
|
143
85
|
end
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
success: true,
|
|
147
|
-
message: "Content written to clipboard",
|
|
148
|
-
length: content.length
|
|
149
|
-
}
|
|
150
86
|
end
|
|
151
87
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
else
|
|
162
|
-
raise "No clipboard tool found. Install xclip or xsel."
|
|
163
|
-
end
|
|
164
|
-
when :windows
|
|
165
|
-
IO.popen('clip', 'w') { |io| io.print '' }
|
|
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) }
|
|
166
97
|
else
|
|
167
|
-
raise "
|
|
98
|
+
raise "Clipboard not supported on this platform. Install xclip or xsel on Linux."
|
|
168
99
|
end
|
|
100
|
+
end
|
|
169
101
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
message: "Clipboard cleared"
|
|
173
|
-
}
|
|
102
|
+
def macos?
|
|
103
|
+
RUBY_PLATFORM.include?('darwin')
|
|
174
104
|
end
|
|
175
105
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
when /mswin|mingw|cygwin/
|
|
183
|
-
:windows
|
|
184
|
-
else
|
|
185
|
-
:unknown
|
|
186
|
-
end
|
|
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')
|
|
187
112
|
end
|
|
188
113
|
|
|
189
|
-
def
|
|
190
|
-
system(
|
|
114
|
+
def linux_xsel?
|
|
115
|
+
@linux_xsel ||= system('which xsel > /dev/null 2>&1')
|
|
191
116
|
end
|
|
192
117
|
end
|
|
193
118
|
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}")
|