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,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
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../shared_tools'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
# Returns OS, CPU, memory, disk, network, and Ruby runtime information.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# tool = SharedTools::Tools::SystemInfoTool.new
|
|
11
|
+
# tool.execute # all categories
|
|
12
|
+
# tool.execute(category: 'cpu') # CPU only
|
|
13
|
+
class SystemInfoTool < ::RubyLLM::Tool
|
|
14
|
+
def self.name = 'system_info_tool'
|
|
15
|
+
|
|
16
|
+
description <<~DESC
|
|
17
|
+
Retrieve system information from the local machine.
|
|
18
|
+
|
|
19
|
+
Categories:
|
|
20
|
+
- 'os' — Operating system name, version, hostname
|
|
21
|
+
- 'cpu' — CPU model, core count, load averages
|
|
22
|
+
- 'memory' — Total and available RAM in GB
|
|
23
|
+
- 'disk' — Mounted filesystems with used/available space
|
|
24
|
+
- 'network' — Active network interfaces and their IP addresses
|
|
25
|
+
- 'ruby' — Ruby version, platform, engine, RubyGems version
|
|
26
|
+
- 'all' (default) — All of the above combined
|
|
27
|
+
DESC
|
|
28
|
+
|
|
29
|
+
params do
|
|
30
|
+
string :category, required: false, description: <<~DESC.strip
|
|
31
|
+
Info category. Options: 'os', 'cpu', 'memory', 'disk', 'network', 'ruby', 'all' (default).
|
|
32
|
+
DESC
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param logger [Logger] optional logger
|
|
36
|
+
def initialize(logger: nil)
|
|
37
|
+
@logger = logger || RubyLLM.logger
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param category [String] which subsystem to query
|
|
41
|
+
# @return [Hash] system information
|
|
42
|
+
def execute(category: 'all')
|
|
43
|
+
@logger.info("SystemInfoTool#execute category=#{category}")
|
|
44
|
+
|
|
45
|
+
case category.to_s.downcase
|
|
46
|
+
when 'os' then { success: true }.merge(os_info)
|
|
47
|
+
when 'cpu' then { success: true }.merge(cpu_info)
|
|
48
|
+
when 'memory' then { success: true }.merge(memory_info)
|
|
49
|
+
when 'disk' then { success: true }.merge(disk_info)
|
|
50
|
+
when 'network' then { success: true }.merge(network_info)
|
|
51
|
+
when 'ruby' then { success: true }.merge(ruby_info)
|
|
52
|
+
else
|
|
53
|
+
{ success: true }
|
|
54
|
+
.merge(os_info)
|
|
55
|
+
.merge(cpu_info)
|
|
56
|
+
.merge(memory_info)
|
|
57
|
+
.merge(disk_info)
|
|
58
|
+
.merge(network_info)
|
|
59
|
+
.merge(ruby_info)
|
|
60
|
+
end
|
|
61
|
+
rescue => e
|
|
62
|
+
@logger.error("SystemInfoTool error: #{e.message}")
|
|
63
|
+
{ success: false, error: e.message }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def os_info
|
|
69
|
+
{
|
|
70
|
+
os_platform: RUBY_PLATFORM,
|
|
71
|
+
os_name: detect_os_name,
|
|
72
|
+
os_version: detect_os_version,
|
|
73
|
+
hostname: `hostname`.strip
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cpu_info
|
|
78
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
79
|
+
model = `sysctl -n machdep.cpu.brand_string 2>/dev/null`.strip
|
|
80
|
+
cores = `sysctl -n hw.ncpu 2>/dev/null`.strip.to_i
|
|
81
|
+
load = `sysctl -n vm.loadavg 2>/dev/null`.strip
|
|
82
|
+
.gsub(/[{}]/, '').split.first(3).map(&:to_f)
|
|
83
|
+
else
|
|
84
|
+
model = File.read('/proc/cpuinfo')
|
|
85
|
+
.match(/model name\s*:\s*(.+)/)&.captures&.first&.strip rescue 'Unknown'
|
|
86
|
+
cores = `nproc 2>/dev/null`.strip.to_i
|
|
87
|
+
load = File.read('/proc/loadavg').split.first(3).map(&:to_f) rescue [0.0, 0.0, 0.0]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
cpu_model: model.empty? ? 'Unknown' : model,
|
|
92
|
+
cpu_cores: cores,
|
|
93
|
+
load_avg_1m: load[0],
|
|
94
|
+
load_avg_5m: load[1],
|
|
95
|
+
load_avg_15m: load[2]
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def memory_info
|
|
100
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
101
|
+
total = `sysctl -n hw.memsize 2>/dev/null`.strip.to_i
|
|
102
|
+
vm_stat = `vm_stat 2>/dev/null`
|
|
103
|
+
pg_size = vm_stat.match(/page size of (\d+) bytes/)&.captures&.first&.to_i || 4096
|
|
104
|
+
free_pg = vm_stat.match(/Pages free:\s+(\d+)/)&.captures&.first&.to_i || 0
|
|
105
|
+
inact_pg = vm_stat.match(/Pages inactive:\s+(\d+)/)&.captures&.first&.to_i || 0
|
|
106
|
+
available = (free_pg + inact_pg) * pg_size
|
|
107
|
+
else
|
|
108
|
+
mem = File.read('/proc/meminfo') rescue ''
|
|
109
|
+
total = (mem.match(/MemTotal:\s+(\d+) kB/)&.captures&.first&.to_i || 0) * 1024
|
|
110
|
+
available = (mem.match(/MemAvailable:\s+(\d+) kB/)&.captures&.first&.to_i || 0) * 1024
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
gb = 1024.0**3
|
|
114
|
+
{
|
|
115
|
+
memory_total_gb: (total / gb).round(2),
|
|
116
|
+
memory_available_gb: (available / gb).round(2),
|
|
117
|
+
memory_used_gb: ((total - available) / gb).round(2)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def disk_info
|
|
122
|
+
lines = `df -k 2>/dev/null`.lines.drop(1)
|
|
123
|
+
mounts = lines.filter_map do |line|
|
|
124
|
+
parts = line.split
|
|
125
|
+
next unless parts.size >= 6
|
|
126
|
+
|
|
127
|
+
kb = 1024.0**2
|
|
128
|
+
{
|
|
129
|
+
filesystem: parts[0],
|
|
130
|
+
mount_point: parts[5],
|
|
131
|
+
size_gb: (parts[1].to_i / kb).round(2),
|
|
132
|
+
used_gb: (parts[2].to_i / kb).round(2),
|
|
133
|
+
available_gb: (parts[3].to_i / kb).round(2),
|
|
134
|
+
use_percent: parts[4]
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
{ disks: mounts }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def network_info
|
|
141
|
+
interfaces = {}
|
|
142
|
+
|
|
143
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
144
|
+
current = nil
|
|
145
|
+
`ifconfig 2>/dev/null`.lines.each do |line|
|
|
146
|
+
if (m = line.match(/^(\w[\w:]+\d+):/))
|
|
147
|
+
current = m.captures.first
|
|
148
|
+
interfaces[current] = []
|
|
149
|
+
elsif current && (m = line.match(/\s+inet6?\s+(\S+)/))
|
|
150
|
+
addr = m.captures.first.split('%').first
|
|
151
|
+
interfaces[current] << addr
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
current = nil
|
|
156
|
+
`ip addr 2>/dev/null`.lines.each do |line|
|
|
157
|
+
if (m = line.match(/^\d+: (\w+):/))
|
|
158
|
+
current = m.captures.first
|
|
159
|
+
interfaces[current] = []
|
|
160
|
+
elsif current && (m = line.match(/\s+inet6?\s+(\S+)/))
|
|
161
|
+
interfaces[current] << m.captures.first.split('/').first
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
{ network_interfaces: interfaces.reject { |_, ips| ips.empty? } }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def ruby_info
|
|
170
|
+
{
|
|
171
|
+
ruby_version: RUBY_VERSION,
|
|
172
|
+
ruby_platform: RUBY_PLATFORM,
|
|
173
|
+
ruby_engine: RUBY_ENGINE,
|
|
174
|
+
ruby_description: RUBY_DESCRIPTION,
|
|
175
|
+
rubygems_version: Gem::VERSION
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def detect_os_name
|
|
180
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
181
|
+
`sw_vers -productName 2>/dev/null`.strip
|
|
182
|
+
elsif File.exist?('/etc/os-release')
|
|
183
|
+
File.read('/etc/os-release').match(/^NAME="?([^"\n]+)"?/)&.captures&.first || 'Linux'
|
|
184
|
+
else
|
|
185
|
+
'Unknown'
|
|
186
|
+
end
|
|
187
|
+
rescue
|
|
188
|
+
'Unknown'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def detect_os_version
|
|
192
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
193
|
+
`sw_vers -productVersion 2>/dev/null`.strip
|
|
194
|
+
elsif File.exist?('/etc/os-release')
|
|
195
|
+
File.read('/etc/os-release').match(/^VERSION="?([^"\n]+)"?/)&.captures&.first || 'Unknown'
|
|
196
|
+
else
|
|
197
|
+
'Unknown'
|
|
198
|
+
end
|
|
199
|
+
rescue
|
|
200
|
+
'Unknown'
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -34,6 +34,7 @@ module SharedTools
|
|
|
34
34
|
- 'step': Execute the next step in an existing workflow using provided step data
|
|
35
35
|
- 'status': Check the current status and progress of an existing workflow
|
|
36
36
|
- 'complete': Mark a workflow as finished and clean up associated resources
|
|
37
|
+
- 'list': List all existing workflows with their current status and summary information
|
|
37
38
|
Each action requires different combinations of the other parameters.
|
|
38
39
|
DESC
|
|
39
40
|
|
|
@@ -84,6 +85,8 @@ module SharedTools
|
|
|
84
85
|
when "complete"
|
|
85
86
|
return {success: false, error: "workflow_id required for 'complete' action"} unless workflow_id
|
|
86
87
|
complete_workflow(workflow_id)
|
|
88
|
+
when "list"
|
|
89
|
+
list_workflows
|
|
87
90
|
else
|
|
88
91
|
{success: false, error: "Unknown action: #{action}"}
|
|
89
92
|
end
|
|
@@ -279,6 +282,35 @@ module SharedTools
|
|
|
279
282
|
}
|
|
280
283
|
end
|
|
281
284
|
|
|
285
|
+
# List all workflows in the storage directory
|
|
286
|
+
def list_workflows
|
|
287
|
+
pattern = File.join(@storage_dir, "workflow_*.json")
|
|
288
|
+
files = Dir.glob(pattern)
|
|
289
|
+
|
|
290
|
+
workflows = files.filter_map do |file|
|
|
291
|
+
state = JSON.parse(File.read(file), symbolize_names: true)
|
|
292
|
+
{
|
|
293
|
+
workflow_id: state[:id],
|
|
294
|
+
status: state[:status],
|
|
295
|
+
step_count: state[:steps]&.length || 0,
|
|
296
|
+
created_at: state[:created_at],
|
|
297
|
+
updated_at: state[:updated_at],
|
|
298
|
+
completed_at: state[:completed_at]
|
|
299
|
+
}
|
|
300
|
+
rescue => e
|
|
301
|
+
@logger.warn("Skipping unreadable workflow file #{file}: #{e.message}")
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
workflows.sort_by! { |w| w[:created_at] || "" }
|
|
306
|
+
|
|
307
|
+
{
|
|
308
|
+
success: true,
|
|
309
|
+
total: workflows.size,
|
|
310
|
+
workflows: workflows
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
282
314
|
# Save workflow state to disk
|
|
283
315
|
def save_workflow_state(workflow_id, state)
|
|
284
316
|
file_path = workflow_file_path(workflow_id)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# lib/shared_tools/utilities.rb
|
|
4
|
+
#
|
|
5
|
+
# General-purpose utility methods for SharedTools MCP clients and tools.
|
|
6
|
+
# Loaded automatically by lib/shared_tools.rb.
|
|
7
|
+
|
|
8
|
+
module SharedTools
|
|
9
|
+
MCP_LOG_MUTEX = Mutex.new
|
|
10
|
+
@mcp_load_log = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# MCP load tracking
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
# Record the outcome of loading a single MCP client.
|
|
19
|
+
# Called by mcp.rb from within each loader thread.
|
|
20
|
+
#
|
|
21
|
+
# SharedTools.record_mcp_result("tavily") # success
|
|
22
|
+
# SharedTools.record_mcp_result("notion", error: e) # failure
|
|
23
|
+
def record_mcp_result(name, error: nil)
|
|
24
|
+
MCP_LOG_MUTEX.synchronize do
|
|
25
|
+
@mcp_load_log[name] = error ? { status: :failed, reason: error.message } : { status: :ok }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns an array of client names that loaded successfully.
|
|
30
|
+
#
|
|
31
|
+
# SharedTools.mcp_loaded #=> ["github", "memory", "chart"]
|
|
32
|
+
def mcp_loaded
|
|
33
|
+
MCP_LOG_MUTEX.synchronize { @mcp_load_log.select { |_, v| v[:status] == :ok }.keys }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns a hash of client names that failed to load, mapped to their error messages.
|
|
37
|
+
#
|
|
38
|
+
# SharedTools.mcp_failed #=> {"tavily" => "Missing envars: TAVILY_API_KEY", ...}
|
|
39
|
+
def mcp_failed
|
|
40
|
+
MCP_LOG_MUTEX.synchronize do
|
|
41
|
+
@mcp_load_log.select { |_, v| v[:status] == :failed }.transform_values { |v| v[:reason] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Prints a summary table of MCP client load results and returns the full log hash.
|
|
46
|
+
#
|
|
47
|
+
# SharedTools.mcp_status
|
|
48
|
+
# # MCP Client Status
|
|
49
|
+
# # ✓ github
|
|
50
|
+
# # ✓ memory
|
|
51
|
+
# # ✗ tavily — Missing envars: TAVILY_API_KEY
|
|
52
|
+
def mcp_status
|
|
53
|
+
log = MCP_LOG_MUTEX.synchronize { @mcp_load_log.dup }
|
|
54
|
+
|
|
55
|
+
if log.empty?
|
|
56
|
+
puts "No MCP clients have been loaded yet. Try: require 'shared_tools/mcp'"
|
|
57
|
+
return log
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts "MCP Client Status"
|
|
61
|
+
puts "-" * 40
|
|
62
|
+
log.sort.each do |name, entry|
|
|
63
|
+
if entry[:status] == :ok
|
|
64
|
+
puts " \u2713 #{name}"
|
|
65
|
+
else
|
|
66
|
+
puts " \u2717 #{name} \u2014 #{entry[:reason]}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
puts "-" * 40
|
|
70
|
+
puts " #{log.count { |_, v| v[:status] == :ok }} loaded, " \
|
|
71
|
+
"#{log.count { |_, v| v[:status] == :failed }} skipped"
|
|
72
|
+
|
|
73
|
+
log
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns true if all named environment variables are set and non-empty.
|
|
77
|
+
# Warns for each missing variable and returns false if any are absent.
|
|
78
|
+
#
|
|
79
|
+
# SharedTools.verify_envars("GITHUB_PERSONAL_ACCESS_TOKEN")
|
|
80
|
+
# SharedTools.verify_envars("TAVILY_API_KEY", "ANOTHER_KEY")
|
|
81
|
+
def verify_envars(*names)
|
|
82
|
+
missing = names.select { |n| ENV.fetch(n, "").empty? }
|
|
83
|
+
missing.each { |n| warn "SharedTools — #{n} is not set" }
|
|
84
|
+
unless missing.empty?
|
|
85
|
+
raise LoadError, "Missing envars: #{missing.join(', ')}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# High-level package installer. Detects the current platform and calls the
|
|
90
|
+
# appropriate *_install method. Raises LoadError if any package cannot be installed.
|
|
91
|
+
#
|
|
92
|
+
# SharedTools.package_install("github-mcp-server")
|
|
93
|
+
# SharedTools.package_install("curl", "jq")
|
|
94
|
+
def package_install(*packages)
|
|
95
|
+
case RUBY_PLATFORM
|
|
96
|
+
when /darwin/
|
|
97
|
+
brew_install(*packages)
|
|
98
|
+
when /linux/
|
|
99
|
+
if system("which apt-get > /dev/null 2>&1")
|
|
100
|
+
apt_install(*packages)
|
|
101
|
+
elsif system("which dnf > /dev/null 2>&1")
|
|
102
|
+
dnf_install(*packages)
|
|
103
|
+
elsif system("which brew > /dev/null 2>&1")
|
|
104
|
+
brew_install(*packages)
|
|
105
|
+
else
|
|
106
|
+
raise LoadError, "No supported package manager found (apt-get, dnf, brew)"
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
raise LoadError, "Unsupported platform: #{RUBY_PLATFORM}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Ensures each named binary is available in PATH, installing via brew if missing.
|
|
114
|
+
# Raises LoadError if brew is not installed or any package install fails.
|
|
115
|
+
#
|
|
116
|
+
# SharedTools.brew_install("github-mcp-server")
|
|
117
|
+
# SharedTools.brew_install("gh", "jq")
|
|
118
|
+
def brew_install(*packages)
|
|
119
|
+
raise LoadError, "Homebrew is not installed (https://brew.sh)" unless system("which brew > /dev/null 2>&1")
|
|
120
|
+
|
|
121
|
+
packages.each do |pkg|
|
|
122
|
+
next unless `brew list --versions #{pkg} 2>/dev/null`.strip.empty?
|
|
123
|
+
|
|
124
|
+
warn "SharedTools — #{pkg} not found, installing via brew..."
|
|
125
|
+
raise LoadError, "#{pkg} could not be installed" unless system("brew install --quiet #{pkg} > /dev/null 2>&1")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Ensures each named binary is available in PATH, installing via apt-get if missing.
|
|
130
|
+
# Raises LoadError if any package install fails.
|
|
131
|
+
#
|
|
132
|
+
# SharedTools.apt_install("curl")
|
|
133
|
+
# SharedTools.apt_install("curl", "jq")
|
|
134
|
+
def apt_install(*packages)
|
|
135
|
+
packages.each do |pkg|
|
|
136
|
+
# SMELL: what if package is a library?
|
|
137
|
+
next if system("which #{pkg} > /dev/null 2>&1")
|
|
138
|
+
|
|
139
|
+
warn "SharedTools — #{pkg} not found, installing via apt-get..."
|
|
140
|
+
raise LoadError, "#{pkg} could not be installed" unless system("sudo apt-get install -y -q #{pkg} > /dev/null 2>&1")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Ensures each named binary is available in PATH, installing via dnf if missing.
|
|
145
|
+
# Raises LoadError if any package install fails.
|
|
146
|
+
#
|
|
147
|
+
# SharedTools.dnf_install("curl")
|
|
148
|
+
# SharedTools.dnf_install("curl", "jq")
|
|
149
|
+
def dnf_install(*packages)
|
|
150
|
+
packages.each do |pkg|
|
|
151
|
+
# SMELL: What if package is a library?
|
|
152
|
+
next if system("which #{pkg} > /dev/null 2>&1")
|
|
153
|
+
|
|
154
|
+
warn "SharedTools — #{pkg} not found, installing via dnf..."
|
|
155
|
+
raise LoadError, "#{pkg} could not be installed" unless system("sudo dnf install -y -q #{pkg} > /dev/null 2>&1")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Ensures each named npm package binary is available in PATH, installing
|
|
160
|
+
# globally via npm if missing. Raises LoadError if npm is not found or any install fails.
|
|
161
|
+
#
|
|
162
|
+
# SharedTools.npm_install("typescript")
|
|
163
|
+
# SharedTools.npm_install("typescript", "ts-node")
|
|
164
|
+
def npm_install(*packages)
|
|
165
|
+
raise LoadError, "npm is not installed (https://nodejs.org)" unless system("which npm > /dev/null 2>&1")
|
|
166
|
+
|
|
167
|
+
packages.each do |pkg|
|
|
168
|
+
# SMELL: What if package is a library?
|
|
169
|
+
next if system("which #{pkg} > /dev/null 2>&1")
|
|
170
|
+
|
|
171
|
+
warn "SharedTools — #{pkg} not found, installing via npm..."
|
|
172
|
+
raise LoadError, "#{pkg} could not be installed" unless system("npm install -g --silent #{pkg} > /dev/null 2>&1")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Ensures each named gem is available, installing via gem install if missing.
|
|
177
|
+
# Raises LoadError if gem is not available or any install fails.
|
|
178
|
+
#
|
|
179
|
+
# SharedTools.gem_install("nokogiri")
|
|
180
|
+
# SharedTools.gem_install("nokogiri", "oj")
|
|
181
|
+
def gem_install(*packages)
|
|
182
|
+
raise LoadError, "gem is not available" unless system("which gem > /dev/null 2>&1")
|
|
183
|
+
|
|
184
|
+
packages.each do |pkg|
|
|
185
|
+
next if system("gem list -i #{pkg} > /dev/null 2>&1")
|
|
186
|
+
|
|
187
|
+
warn "SharedTools — #{pkg} not found, installing via gem..."
|
|
188
|
+
raise LoadError, "#{pkg} could not be installed" unless system("gem install --silent #{pkg} > /dev/null 2>&1")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/shared_tools/version.rb
CHANGED