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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -16
  3. data/README.md +257 -262
  4. data/lib/shared_tools/browser_tool.rb +5 -0
  5. data/lib/shared_tools/calculator_tool.rb +4 -0
  6. data/lib/shared_tools/clipboard_tool.rb +4 -0
  7. data/lib/shared_tools/composite_analysis_tool.rb +4 -0
  8. data/lib/shared_tools/computer_tool.rb +5 -0
  9. data/lib/shared_tools/cron_tool.rb +4 -0
  10. data/lib/shared_tools/current_date_time_tool.rb +4 -0
  11. data/lib/shared_tools/data_science_kit.rb +4 -0
  12. data/lib/shared_tools/database.rb +4 -0
  13. data/lib/shared_tools/database_query_tool.rb +4 -0
  14. data/lib/shared_tools/database_tool.rb +5 -0
  15. data/lib/shared_tools/disk_tool.rb +5 -0
  16. data/lib/shared_tools/dns_tool.rb +4 -0
  17. data/lib/shared_tools/doc_tool.rb +5 -0
  18. data/lib/shared_tools/error_handling_tool.rb +4 -0
  19. data/lib/shared_tools/eval_tool.rb +5 -0
  20. data/lib/shared_tools/mcp/brave_search_client.rb +37 -0
  21. data/lib/shared_tools/mcp/chart_client.rb +32 -0
  22. data/lib/shared_tools/mcp/github_client.rb +38 -0
  23. data/lib/shared_tools/mcp/hugging_face_client.rb +43 -0
  24. data/lib/shared_tools/mcp/memory_client.rb +33 -0
  25. data/lib/shared_tools/mcp/notion_client.rb +40 -0
  26. data/lib/shared_tools/mcp/sequential_thinking_client.rb +33 -0
  27. data/lib/shared_tools/mcp/slack_client.rb +54 -0
  28. data/lib/shared_tools/mcp/streamable_http_patch.rb +42 -0
  29. data/lib/shared_tools/mcp/tavily_client.rb +41 -0
  30. data/lib/shared_tools/mcp.rb +45 -16
  31. data/lib/shared_tools/system_info_tool.rb +4 -0
  32. data/lib/shared_tools/tools/browser/base_tool.rb +8 -12
  33. data/lib/shared_tools/tools/browser/click_tool.rb +4 -2
  34. data/lib/shared_tools/tools/browser/ferrum_driver.rb +119 -0
  35. data/lib/shared_tools/tools/browser/inspect_tool.rb +4 -2
  36. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +4 -2
  37. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +19 -7
  38. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +4 -2
  39. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +4 -2
  40. data/lib/shared_tools/tools/browser/visit_tool.rb +4 -2
  41. data/lib/shared_tools/tools/browser.rb +31 -2
  42. data/lib/shared_tools/tools/browser_tool.rb +6 -0
  43. data/lib/shared_tools/tools/clipboard_tool.rb +69 -144
  44. data/lib/shared_tools/tools/composite_analysis_tool.rb +60 -4
  45. data/lib/shared_tools/tools/computer/mac_driver.rb +37 -4
  46. data/lib/shared_tools/tools/cron_tool.rb +237 -379
  47. data/lib/shared_tools/tools/current_date_time_tool.rb +54 -120
  48. data/lib/shared_tools/tools/data_science_kit.rb +63 -13
  49. data/lib/shared_tools/tools/dns_tool.rb +335 -269
  50. data/lib/shared_tools/tools/doc/docx_reader_tool.rb +107 -0
  51. data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +171 -0
  52. data/lib/shared_tools/tools/doc/text_reader_tool.rb +57 -0
  53. data/lib/shared_tools/tools/doc.rb +3 -0
  54. data/lib/shared_tools/tools/doc_tool.rb +101 -6
  55. data/lib/shared_tools/tools/docker/compose_run_tool.rb +1 -1
  56. data/lib/shared_tools/tools/enabler.rb +42 -0
  57. data/lib/shared_tools/tools/error_handling_tool.rb +3 -1
  58. data/lib/shared_tools/tools/notification/base_driver.rb +51 -0
  59. data/lib/shared_tools/tools/notification/linux_driver.rb +115 -0
  60. data/lib/shared_tools/tools/notification/mac_driver.rb +66 -0
  61. data/lib/shared_tools/tools/notification/null_driver.rb +29 -0
  62. data/lib/shared_tools/tools/notification.rb +12 -0
  63. data/lib/shared_tools/tools/notification_tool.rb +99 -0
  64. data/lib/shared_tools/tools/system_info_tool.rb +130 -343
  65. data/lib/shared_tools/tools/workflow_manager_tool.rb +32 -0
  66. data/lib/shared_tools/utilities.rb +193 -0
  67. data/lib/shared_tools/version.rb +1 -1
  68. data/lib/shared_tools/weather_tool.rb +4 -0
  69. data/lib/shared_tools/workflow_manager_tool.rb +4 -0
  70. data/lib/shared_tools.rb +28 -38
  71. metadata +74 -9
  72. data/lib/shared_tools/mcp/github_mcp_server.rb +0 -58
  73. data/lib/shared_tools/mcp/imcp.rb +0 -28
  74. data/lib/shared_tools/mcp/tavily_mcp_server.rb +0 -44
  75. 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