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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Notification
|
|
8
|
+
# Linux notification driver.
|
|
9
|
+
#
|
|
10
|
+
# notify — uses notify-send (libnotify); logs a warning if no display is available.
|
|
11
|
+
# alert — uses zenity when a display is present; falls back to a terminal prompt.
|
|
12
|
+
# speak — tries espeak-ng first, then espeak.
|
|
13
|
+
class LinuxDriver < BaseDriver
|
|
14
|
+
# @param message [String]
|
|
15
|
+
# @param title [String, nil]
|
|
16
|
+
# @param subtitle [String, nil] appended to the message body
|
|
17
|
+
# @param sound [String, nil] ignored on Linux
|
|
18
|
+
# @return [Hash]
|
|
19
|
+
def notify(message:, title: nil, subtitle: nil, sound: nil)
|
|
20
|
+
unless display_available?
|
|
21
|
+
RubyLLM.logger.warn('NotificationTool: No display server available, cannot show notification')
|
|
22
|
+
return { success: false, error: 'No display server available' }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless command_available?('notify-send')
|
|
26
|
+
return { success: false, error: 'notify-send not found. Install libnotify-bin (Debian/Ubuntu) or libnotify (Fedora/Arch).' }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
body = [message, subtitle].compact.join("\n")
|
|
30
|
+
cmd = ['notify-send', (title || 'Notification'), body]
|
|
31
|
+
_, stderr, status = Open3.capture3(*cmd)
|
|
32
|
+
status.success? ? { success: true, action: 'notify' } : { success: false, error: stderr.strip }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param message [String]
|
|
36
|
+
# @param title [String, nil]
|
|
37
|
+
# @param buttons [Array<String>]
|
|
38
|
+
# @param default_button [String, nil]
|
|
39
|
+
# @return [Hash] includes :button with the label of the clicked button
|
|
40
|
+
def alert(message:, title: nil, buttons: ['OK'], default_button: nil)
|
|
41
|
+
if display_available? && command_available?('zenity')
|
|
42
|
+
alert_zenity(message:, title:, buttons:, default_button:)
|
|
43
|
+
else
|
|
44
|
+
alert_terminal(message:, buttons:, default_button:)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param text [String]
|
|
49
|
+
# @param voice [String, nil] espeak voice name (e.g. 'en', 'en-us')
|
|
50
|
+
# @param rate [Integer, nil] words per minute (espeak -s flag)
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def speak(text:, voice: nil, rate: nil)
|
|
53
|
+
binary = espeak_binary
|
|
54
|
+
unless binary
|
|
55
|
+
return { success: false, error: 'espeak-ng or espeak not found. Install espeak-ng (recommended) or espeak.' }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
cmd = [binary, text]
|
|
59
|
+
cmd += ['-v', voice] if voice
|
|
60
|
+
cmd += ['-s', rate.to_s] if rate
|
|
61
|
+
_, stderr, status = Open3.capture3(*cmd)
|
|
62
|
+
status.success? ? { success: true, action: 'speak' } : { success: false, error: stderr.strip }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def display_available?
|
|
68
|
+
ENV['DISPLAY'] || ENV['WAYLAND_DISPLAY']
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def espeak_binary
|
|
72
|
+
return 'espeak-ng' if command_available?('espeak-ng')
|
|
73
|
+
return 'espeak' if command_available?('espeak')
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def alert_zenity(message:, title:, buttons:, default_button:)
|
|
78
|
+
if buttons.length == 1
|
|
79
|
+
cmd = ['zenity', '--info', '--text', message]
|
|
80
|
+
cmd += ['--title', title] if title
|
|
81
|
+
_, stderr, status = Open3.capture3(*cmd)
|
|
82
|
+
status.success? ? { success: true, button: buttons.first } : { success: false, error: stderr.strip }
|
|
83
|
+
else
|
|
84
|
+
ok_label = buttons[0]
|
|
85
|
+
cancel_label = buttons[1]
|
|
86
|
+
cmd = ['zenity', '--question', '--text', message,
|
|
87
|
+
'--ok-label', ok_label, '--cancel-label', cancel_label]
|
|
88
|
+
cmd += ['--title', title] if title
|
|
89
|
+
# zenity supports --extra-button for additional buttons beyond two
|
|
90
|
+
buttons[2..].each { |b| cmd += ['--extra-button', b] } if buttons.length > 2
|
|
91
|
+
|
|
92
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
93
|
+
case status.exitstatus
|
|
94
|
+
when 0 then { success: true, button: ok_label }
|
|
95
|
+
when 1 then { success: true, button: cancel_label }
|
|
96
|
+
else
|
|
97
|
+
btn = stdout.strip
|
|
98
|
+
btn.empty? ? { success: false, error: stderr.strip } : { success: true, button: btn }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def alert_terminal(message:, buttons:, default_button:)
|
|
104
|
+
$stdout.puts "\n[ALERT] #{message}"
|
|
105
|
+
$stdout.puts "Options: #{buttons.each_with_index.map { |b, i| "#{i + 1}) #{b}" }.join(' ')}"
|
|
106
|
+
$stdout.print "Enter choice (1-#{buttons.length}): "
|
|
107
|
+
$stdout.flush
|
|
108
|
+
input = $stdin.gets&.strip.to_i
|
|
109
|
+
button = buttons[input - 1] || default_button || buttons.first
|
|
110
|
+
{ success: true, button: button }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Notification
|
|
8
|
+
# macOS notification driver using osascript and the say command.
|
|
9
|
+
class MacDriver < BaseDriver
|
|
10
|
+
# @param message [String]
|
|
11
|
+
# @param title [String, nil]
|
|
12
|
+
# @param subtitle [String, nil]
|
|
13
|
+
# @param sound [String, nil] e.g. 'Glass', 'Ping'
|
|
14
|
+
# @return [Hash]
|
|
15
|
+
def notify(message:, title: nil, subtitle: nil, sound: nil)
|
|
16
|
+
parts = ["display notification #{message.inspect}"]
|
|
17
|
+
parts << "with title #{title.inspect}" if title
|
|
18
|
+
parts << "subtitle #{subtitle.inspect}" if subtitle
|
|
19
|
+
parts << "sound name #{sound.inspect}" if sound
|
|
20
|
+
run_osascript(parts.join(' '))
|
|
21
|
+
.then { |r| r[:success] ? r.merge(action: 'notify') : r }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param message [String]
|
|
25
|
+
# @param title [String, nil]
|
|
26
|
+
# @param buttons [Array<String>]
|
|
27
|
+
# @param default_button [String, nil]
|
|
28
|
+
# @return [Hash] includes :button with label of clicked button
|
|
29
|
+
def alert(message:, title: nil, buttons: ['OK'], default_button: nil)
|
|
30
|
+
btn_list = buttons.map(&:inspect).join(', ')
|
|
31
|
+
script = "display dialog #{message.inspect}"
|
|
32
|
+
script += " with title #{title.inspect}" if title
|
|
33
|
+
script += " buttons {#{btn_list}}"
|
|
34
|
+
script += " default button #{default_button.inspect}" if default_button
|
|
35
|
+
|
|
36
|
+
stdout, stderr, status = Open3.capture3('osascript', '-e', script)
|
|
37
|
+
if status.success?
|
|
38
|
+
button = stdout.match(/button returned:(.+)/i)&.captures&.first&.strip
|
|
39
|
+
{ success: true, button: button }
|
|
40
|
+
else
|
|
41
|
+
{ success: false, error: stderr.strip }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param text [String]
|
|
46
|
+
# @param voice [String, nil] e.g. 'Samantha'
|
|
47
|
+
# @param rate [Integer, nil] words per minute
|
|
48
|
+
# @return [Hash]
|
|
49
|
+
def speak(text:, voice: nil, rate: nil)
|
|
50
|
+
cmd = ['say', text]
|
|
51
|
+
cmd += ['-v', voice] if voice
|
|
52
|
+
cmd += ['-r', rate.to_s] if rate
|
|
53
|
+
_, stderr, status = Open3.capture3(*cmd)
|
|
54
|
+
status.success? ? { success: true, action: 'speak' } : { success: false, error: stderr.strip }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def run_osascript(script)
|
|
60
|
+
_, stderr, status = Open3.capture3('osascript', '-e', script)
|
|
61
|
+
status.success? ? { success: true } : { success: false, error: stderr.strip }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Notification
|
|
6
|
+
# Fallback driver for unsupported platforms.
|
|
7
|
+
# All actions return a failure response with a clear error message.
|
|
8
|
+
class NullDriver < BaseDriver
|
|
9
|
+
def notify(message:, title: nil, subtitle: nil, sound: nil)
|
|
10
|
+
unsupported
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def alert(message:, title: nil, buttons: ['OK'], default_button: nil)
|
|
14
|
+
unsupported
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def speak(text:, voice: nil, rate: nil)
|
|
18
|
+
unsupported
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def unsupported
|
|
24
|
+
{ success: false, error: "NotificationTool is not supported on platform: #{RUBY_PLATFORM}" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collection loader for notification tools
|
|
4
|
+
# Usage: require 'shared_tools/tools/notification'
|
|
5
|
+
|
|
6
|
+
require 'shared_tools'
|
|
7
|
+
|
|
8
|
+
require_relative 'notification/base_driver'
|
|
9
|
+
require_relative 'notification/mac_driver'
|
|
10
|
+
require_relative 'notification/linux_driver'
|
|
11
|
+
require_relative 'notification/null_driver'
|
|
12
|
+
require_relative 'notification_tool'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
# Cross-platform notification tool for desktop banners, modal dialogs, and TTS.
|
|
6
|
+
#
|
|
7
|
+
# Supports macOS (osascript, say) and Linux (notify-send, zenity/terminal, espeak-ng/espeak).
|
|
8
|
+
# On unsupported platforms all actions return {success: false}.
|
|
9
|
+
#
|
|
10
|
+
# @example Desktop notification
|
|
11
|
+
# tool = SharedTools::Tools::NotificationTool.new
|
|
12
|
+
# tool.execute(action: 'notify', message: 'Build complete', title: 'CI')
|
|
13
|
+
#
|
|
14
|
+
# @example Modal alert
|
|
15
|
+
# result = tool.execute(action: 'alert', message: 'Deploy to production?', buttons: ['Yes', 'No'])
|
|
16
|
+
# result[:button] # => 'Yes' or 'No'
|
|
17
|
+
#
|
|
18
|
+
# @example Text-to-speech
|
|
19
|
+
# tool.execute(action: 'speak', message: 'Task finished')
|
|
20
|
+
class NotificationTool < ::RubyLLM::Tool
|
|
21
|
+
def self.name = 'notification_tool'
|
|
22
|
+
|
|
23
|
+
description <<~DESC.strip
|
|
24
|
+
Send desktop notifications, modal alerts, or text-to-speech messages.
|
|
25
|
+
Supports macOS and Linux. On macOS uses osascript and say.
|
|
26
|
+
On Linux uses notify-send, zenity (or terminal fallback), and espeak-ng/espeak.
|
|
27
|
+
DESC
|
|
28
|
+
|
|
29
|
+
params do
|
|
30
|
+
string :action, description: <<~TEXT.strip
|
|
31
|
+
The notification action to perform:
|
|
32
|
+
* `notify` — Non-blocking desktop banner notification.
|
|
33
|
+
* `alert` — Modal dialog; waits for the user to click a button. Returns the button label.
|
|
34
|
+
* `speak` — Speak text aloud using text-to-speech.
|
|
35
|
+
TEXT
|
|
36
|
+
|
|
37
|
+
string :message, description: "The message to display or speak. Required for all actions."
|
|
38
|
+
|
|
39
|
+
string :title, description: "Title for the notification or alert dialog. Optional.", required: false
|
|
40
|
+
|
|
41
|
+
string :subtitle, description: "Subtitle line (notify action, macOS and Linux). Optional.", required: false
|
|
42
|
+
|
|
43
|
+
string :sound, description: "Sound name to play with a notification (macOS only, e.g. 'Glass', 'Ping'). Optional.", required: false
|
|
44
|
+
|
|
45
|
+
array :buttons, of: :string, description: <<~TEXT.strip, required: false
|
|
46
|
+
Button labels for the alert dialog (e.g. ['Yes', 'No']). Defaults to ['OK'].
|
|
47
|
+
TEXT
|
|
48
|
+
|
|
49
|
+
string :default_button, description: "Default focused button label for the alert dialog. Optional.", required: false
|
|
50
|
+
|
|
51
|
+
string :voice, description: "TTS voice name for the speak action (e.g. 'Samantha' on macOS, 'en' on Linux). Optional.", required: false
|
|
52
|
+
|
|
53
|
+
integer :rate, description: "Speech rate in words per minute for the speak action. Optional.", required: false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param driver [Notification::BaseDriver] optional; auto-detected from platform if omitted
|
|
57
|
+
def initialize(driver: nil)
|
|
58
|
+
@driver = driver || default_driver
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def execute(action:, message: nil, title: nil, subtitle: nil, sound: nil,
|
|
62
|
+
buttons: nil, default_button: nil, voice: nil, rate: nil)
|
|
63
|
+
buttons ||= ['OK']
|
|
64
|
+
|
|
65
|
+
case action
|
|
66
|
+
when 'notify'
|
|
67
|
+
return missing_param('message', 'notify') if blank?(message)
|
|
68
|
+
@driver.notify(message:, title:, subtitle:, sound:)
|
|
69
|
+
when 'alert'
|
|
70
|
+
return missing_param('message', 'alert') if blank?(message)
|
|
71
|
+
@driver.alert(message:, title:, buttons:, default_button:)
|
|
72
|
+
when 'speak'
|
|
73
|
+
return missing_param('message', 'speak') if blank?(message)
|
|
74
|
+
@driver.speak(text: message, voice:, rate:)
|
|
75
|
+
else
|
|
76
|
+
{ success: false, error: "Unknown action: #{action.inspect}. Must be notify, alert, or speak." }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def default_driver
|
|
83
|
+
case RUBY_PLATFORM
|
|
84
|
+
when /darwin/ then Notification::MacDriver.new
|
|
85
|
+
when /linux/ then Notification::LinuxDriver.new
|
|
86
|
+
else Notification::NullDriver.new
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def blank?(value)
|
|
91
|
+
value.nil? || value.to_s.strip.empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def missing_param(param, action)
|
|
95
|
+
{ success: false, error: "'#{param}' is required for the #{action} action" }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|