shared_tools 0.3.0 → 0.3.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.
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm/tool'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # A tool for retrieving system information including OS, CPU, memory, and disk details.
8
+ # Provides cross-platform support for macOS, Linux, and Windows.
9
+ #
10
+ # @example
11
+ # tool = SharedTools::Tools::SystemInfoTool.new
12
+ # result = tool.execute(category: 'all')
13
+ # puts result[:os][:name] # "macOS"
14
+ # puts result[:memory][:total] # "32 GB"
15
+ class SystemInfoTool < RubyLLM::Tool
16
+ def self.name = 'system_info'
17
+
18
+ description <<~'DESCRIPTION'
19
+ Retrieve system information including operating system, CPU, memory, and disk details.
20
+
21
+ This tool provides cross-platform system information:
22
+ - macOS: Uses system_profiler, sysctl, and df commands
23
+ - Linux: Uses /proc filesystem and df command
24
+ - Windows: Uses wmic and powershell commands
25
+
26
+ Categories:
27
+ - 'all': Returns all system information (default)
28
+ - 'os': Operating system information only
29
+ - 'cpu': CPU information only
30
+ - 'memory': Memory information only
31
+ - 'disk': Disk space information only
32
+ - 'network': Network interface information only
33
+
34
+ Example usage:
35
+ tool = SharedTools::Tools::SystemInfoTool.new
36
+
37
+ # Get all system info
38
+ result = tool.execute(category: 'all')
39
+
40
+ # Get specific category
41
+ result = tool.execute(category: 'memory')
42
+ puts result[:total] # Total RAM
43
+ DESCRIPTION
44
+
45
+ params do
46
+ string :category, description: <<~DESC.strip, required: false
47
+ The category of system information to retrieve:
48
+ - 'all' (default): All system information
49
+ - 'os': Operating system details
50
+ - 'cpu': CPU details
51
+ - 'memory': Memory/RAM details
52
+ - 'disk': Disk space details
53
+ - 'network': Network interface details
54
+ DESC
55
+ end
56
+
57
+ # @param logger [Logger] optional logger
58
+ def initialize(logger: nil)
59
+ @logger = logger || RubyLLM.logger
60
+ end
61
+
62
+ # Execute system info retrieval
63
+ #
64
+ # @param category [String] category of info to retrieve
65
+ # @return [Hash] system information
66
+ def execute(category: 'all')
67
+ @logger.info("SystemInfoTool#execute category=#{category.inspect}")
68
+
69
+ case category.to_s.downcase
70
+ when 'all'
71
+ get_all_info
72
+ when 'os'
73
+ { success: true, os: get_os_info }
74
+ when 'cpu'
75
+ { success: true, cpu: get_cpu_info }
76
+ when 'memory'
77
+ { success: true, memory: get_memory_info }
78
+ when 'disk'
79
+ { success: true, disk: get_disk_info }
80
+ when 'network'
81
+ { success: true, network: get_network_info }
82
+ else
83
+ {
84
+ success: false,
85
+ error: "Unknown category: #{category}. Valid categories are: all, os, cpu, memory, disk, network"
86
+ }
87
+ end
88
+ rescue => e
89
+ @logger.error("SystemInfoTool error: #{e.message}")
90
+ {
91
+ success: false,
92
+ error: e.message
93
+ }
94
+ end
95
+
96
+ private
97
+
98
+ def get_all_info
99
+ {
100
+ success: true,
101
+ os: get_os_info,
102
+ cpu: get_cpu_info,
103
+ memory: get_memory_info,
104
+ disk: get_disk_info,
105
+ network: get_network_info,
106
+ ruby: get_ruby_info
107
+ }
108
+ end
109
+
110
+ def get_os_info
111
+ case platform
112
+ when :macos
113
+ {
114
+ name: 'macOS',
115
+ version: `sw_vers -productVersion 2>/dev/null`.strip,
116
+ build: `sw_vers -buildVersion 2>/dev/null`.strip,
117
+ hostname: `hostname 2>/dev/null`.strip,
118
+ kernel: `uname -r 2>/dev/null`.strip,
119
+ architecture: `uname -m 2>/dev/null`.strip,
120
+ uptime: parse_uptime(`uptime 2>/dev/null`.strip)
121
+ }
122
+ when :linux
123
+ {
124
+ name: get_linux_distro,
125
+ version: get_linux_version,
126
+ hostname: `hostname 2>/dev/null`.strip,
127
+ kernel: `uname -r 2>/dev/null`.strip,
128
+ architecture: `uname -m 2>/dev/null`.strip,
129
+ uptime: parse_uptime(`uptime 2>/dev/null`.strip)
130
+ }
131
+ when :windows
132
+ {
133
+ name: 'Windows',
134
+ version: `ver 2>nul`.strip,
135
+ hostname: `hostname 2>nul`.strip,
136
+ architecture: ENV['PROCESSOR_ARCHITECTURE'] || 'unknown'
137
+ }
138
+ else
139
+ { name: 'unknown', platform: RUBY_PLATFORM }
140
+ end
141
+ end
142
+
143
+ def get_cpu_info
144
+ case platform
145
+ when :macos
146
+ {
147
+ model: `sysctl -n machdep.cpu.brand_string 2>/dev/null`.strip,
148
+ cores: `sysctl -n hw.ncpu 2>/dev/null`.strip.to_i,
149
+ physical_cores: `sysctl -n hw.physicalcpu 2>/dev/null`.strip.to_i,
150
+ architecture: `uname -m 2>/dev/null`.strip,
151
+ load_average: get_load_average
152
+ }
153
+ when :linux
154
+ cpu_info = File.read('/proc/cpuinfo') rescue ''
155
+ model = cpu_info[/model name\s*:\s*(.+)/, 1] || 'unknown'
156
+ cores = cpu_info.scan(/^processor/i).count
157
+ {
158
+ model: model,
159
+ cores: cores,
160
+ architecture: `uname -m 2>/dev/null`.strip,
161
+ load_average: get_load_average
162
+ }
163
+ when :windows
164
+ {
165
+ model: `wmic cpu get name 2>nul`.lines[1]&.strip || 'unknown',
166
+ cores: ENV['NUMBER_OF_PROCESSORS']&.to_i || 0,
167
+ architecture: ENV['PROCESSOR_ARCHITECTURE'] || 'unknown'
168
+ }
169
+ else
170
+ { cores: 0, model: 'unknown' }
171
+ end
172
+ end
173
+
174
+ def get_memory_info
175
+ case platform
176
+ when :macos
177
+ total_bytes = `sysctl -n hw.memsize 2>/dev/null`.strip.to_i
178
+ # Get page size and memory statistics
179
+ vm_stat = `vm_stat 2>/dev/null`
180
+ page_size = vm_stat[/page size of (\d+)/, 1]&.to_i || 4096
181
+ pages_free = vm_stat[/Pages free:\s+(\d+)/, 1]&.to_i || 0
182
+ pages_inactive = vm_stat[/Pages inactive:\s+(\d+)/, 1]&.to_i || 0
183
+
184
+ available_bytes = (pages_free + pages_inactive) * page_size
185
+ used_bytes = total_bytes - available_bytes
186
+
187
+ {
188
+ total: format_bytes(total_bytes),
189
+ total_bytes: total_bytes,
190
+ available: format_bytes(available_bytes),
191
+ available_bytes: available_bytes,
192
+ used: format_bytes(used_bytes),
193
+ used_bytes: used_bytes,
194
+ percent_used: ((used_bytes.to_f / total_bytes) * 100).round(1)
195
+ }
196
+ when :linux
197
+ meminfo = File.read('/proc/meminfo') rescue ''
198
+ total_kb = meminfo[/MemTotal:\s+(\d+)/, 1]&.to_i || 0
199
+ available_kb = meminfo[/MemAvailable:\s+(\d+)/, 1]&.to_i || 0
200
+ used_kb = total_kb - available_kb
201
+
202
+ {
203
+ total: format_bytes(total_kb * 1024),
204
+ total_bytes: total_kb * 1024,
205
+ available: format_bytes(available_kb * 1024),
206
+ available_bytes: available_kb * 1024,
207
+ used: format_bytes(used_kb * 1024),
208
+ used_bytes: used_kb * 1024,
209
+ percent_used: total_kb > 0 ? ((used_kb.to_f / total_kb) * 100).round(1) : 0
210
+ }
211
+ when :windows
212
+ # Using powershell for more reliable output
213
+ output = `powershell -command "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory" 2>nul`
214
+ total_kb = output[/TotalVisibleMemorySize\s*:\s*(\d+)/, 1]&.to_i || 0
215
+ free_kb = output[/FreePhysicalMemory\s*:\s*(\d+)/, 1]&.to_i || 0
216
+ used_kb = total_kb - free_kb
217
+
218
+ {
219
+ total: format_bytes(total_kb * 1024),
220
+ total_bytes: total_kb * 1024,
221
+ available: format_bytes(free_kb * 1024),
222
+ available_bytes: free_kb * 1024,
223
+ used: format_bytes(used_kb * 1024),
224
+ used_bytes: used_kb * 1024,
225
+ percent_used: total_kb > 0 ? ((used_kb.to_f / total_kb) * 100).round(1) : 0
226
+ }
227
+ else
228
+ { total: 'unknown', available: 'unknown' }
229
+ end
230
+ end
231
+
232
+ def get_disk_info
233
+ disks = []
234
+
235
+ case platform
236
+ when :macos, :linux
237
+ df_output = `df -h 2>/dev/null`.lines[1..]
238
+ df_output&.each do |line|
239
+ parts = line.split
240
+ next if parts.length < 6
241
+ next unless parts[0].start_with?('/') || parts[5]&.start_with?('/')
242
+
243
+ mount_point = parts[5] || parts[0]
244
+ disks << {
245
+ filesystem: parts[0],
246
+ size: parts[1],
247
+ used: parts[2],
248
+ available: parts[3],
249
+ percent_used: parts[4],
250
+ mount_point: mount_point
251
+ }
252
+ end
253
+ when :windows
254
+ output = `wmic logicaldisk get size,freespace,caption 2>nul`
255
+ output.lines[1..].each do |line|
256
+ parts = line.split
257
+ next if parts.length < 3
258
+
259
+ caption, free_space, size = parts
260
+ next unless size.to_i > 0
261
+
262
+ disks << {
263
+ filesystem: caption,
264
+ size: format_bytes(size.to_i),
265
+ available: format_bytes(free_space.to_i),
266
+ used: format_bytes(size.to_i - free_space.to_i),
267
+ percent_used: "#{((1 - free_space.to_f / size.to_i) * 100).round}%"
268
+ }
269
+ end
270
+ end
271
+
272
+ disks
273
+ end
274
+
275
+ def get_network_info
276
+ interfaces = []
277
+
278
+ case platform
279
+ when :macos
280
+ ifconfig = `ifconfig 2>/dev/null`
281
+ current_interface = nil
282
+
283
+ ifconfig.each_line do |line|
284
+ if line =~ /^(\w+):/
285
+ current_interface = { name: $1, addresses: [] }
286
+ interfaces << current_interface
287
+ elsif current_interface && line =~ /inet (\d+\.\d+\.\d+\.\d+)/
288
+ current_interface[:addresses] << { type: 'IPv4', address: $1 }
289
+ elsif current_interface && line =~ /inet6 ([a-f0-9:]+)/
290
+ current_interface[:addresses] << { type: 'IPv6', address: $1 }
291
+ end
292
+ end
293
+ when :linux
294
+ # Try ip command first, fall back to ifconfig
295
+ output = `ip addr 2>/dev/null`
296
+ if output.empty?
297
+ output = `ifconfig 2>/dev/null`
298
+ end
299
+
300
+ current_interface = nil
301
+ output.each_line do |line|
302
+ if line =~ /^\d+:\s+(\w+):/
303
+ current_interface = { name: $1, addresses: [] }
304
+ interfaces << current_interface
305
+ elsif line =~ /^(\w+):/
306
+ current_interface = { name: $1, addresses: [] }
307
+ interfaces << current_interface
308
+ elsif current_interface && line =~ /inet (\d+\.\d+\.\d+\.\d+)/
309
+ current_interface[:addresses] << { type: 'IPv4', address: $1 }
310
+ elsif current_interface && line =~ /inet6 ([a-f0-9:]+)/
311
+ current_interface[:addresses] << { type: 'IPv6', address: $1 }
312
+ end
313
+ end
314
+ when :windows
315
+ output = `ipconfig 2>nul`
316
+ current_interface = nil
317
+
318
+ output.each_line do |line|
319
+ if line =~ /adapter (.+):/i
320
+ current_interface = { name: $1.strip, addresses: [] }
321
+ interfaces << current_interface
322
+ elsif current_interface && line =~ /IPv4.*:\s*(\d+\.\d+\.\d+\.\d+)/
323
+ current_interface[:addresses] << { type: 'IPv4', address: $1 }
324
+ elsif current_interface && line =~ /IPv6.*:\s*([a-f0-9:]+)/i
325
+ current_interface[:addresses] << { type: 'IPv6', address: $1 }
326
+ end
327
+ end
328
+ end
329
+
330
+ # Filter out interfaces with no addresses
331
+ interfaces.select { |i| !i[:addresses].empty? }
332
+ end
333
+
334
+ def get_ruby_info
335
+ {
336
+ version: RUBY_VERSION,
337
+ platform: RUBY_PLATFORM,
338
+ engine: RUBY_ENGINE,
339
+ engine_version: RUBY_ENGINE_VERSION,
340
+ patchlevel: RUBY_PATCHLEVEL
341
+ }
342
+ end
343
+
344
+ def platform
345
+ case RUBY_PLATFORM
346
+ when /darwin/
347
+ :macos
348
+ when /linux/
349
+ :linux
350
+ when /mswin|mingw|cygwin/
351
+ :windows
352
+ else
353
+ :unknown
354
+ end
355
+ end
356
+
357
+ def format_bytes(bytes)
358
+ return '0 B' if bytes.nil? || bytes == 0
359
+
360
+ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
361
+ exp = (Math.log(bytes) / Math.log(1024)).to_i
362
+ exp = units.length - 1 if exp >= units.length
363
+
364
+ "#{(bytes.to_f / (1024**exp)).round(2)} #{units[exp]}"
365
+ end
366
+
367
+ def get_load_average
368
+ case platform
369
+ when :macos, :linux
370
+ uptime_output = `uptime 2>/dev/null`
371
+ if uptime_output =~ /load average[s]?:\s*([\d.]+),?\s*([\d.]+),?\s*([\d.]+)/
372
+ { '1min' => $1.to_f, '5min' => $2.to_f, '15min' => $3.to_f }
373
+ else
374
+ {}
375
+ end
376
+ else
377
+ {}
378
+ end
379
+ end
380
+
381
+ def parse_uptime(uptime_str)
382
+ # Extract uptime portion from the uptime command output
383
+ if uptime_str =~ /up\s+(.+?),\s+\d+\s+user/
384
+ $1.strip
385
+ elsif uptime_str =~ /up\s+(.+)/
386
+ $1.split(',').first.strip
387
+ else
388
+ uptime_str
389
+ end
390
+ end
391
+
392
+ def get_linux_distro
393
+ if File.exist?('/etc/os-release')
394
+ content = File.read('/etc/os-release')
395
+ content[/^NAME="?([^"\n]+)"?/, 1] || 'Linux'
396
+ elsif File.exist?('/etc/lsb-release')
397
+ content = File.read('/etc/lsb-release')
398
+ content[/DISTRIB_ID=(.+)/, 1] || 'Linux'
399
+ else
400
+ 'Linux'
401
+ end
402
+ end
403
+
404
+ def get_linux_version
405
+ if File.exist?('/etc/os-release')
406
+ content = File.read('/etc/os-release')
407
+ content[/^VERSION="?([^"\n]+)"?/, 1] || ''
408
+ elsif File.exist?('/etc/lsb-release')
409
+ content = File.read('/etc/lsb-release')
410
+ content[/DISTRIB_RELEASE=(.+)/, 1] || ''
411
+ else
412
+ ''
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedTools
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/shared_tools.rb CHANGED
@@ -4,26 +4,67 @@ require 'ruby_llm'
4
4
  require 'io/console'
5
5
 
6
6
  require "zeitwerk"
7
- loader = Zeitwerk::Loader.for_gem
8
- # Ignore aggregate loader files that don't define constants
9
- loader.ignore("#{__dir__}/shared_tools/ruby_llm.rb")
10
- loader.ignore("#{__dir__}/shared_tools/tools/browser.rb")
11
- loader.ignore("#{__dir__}/shared_tools/tools/computer.rb")
12
- loader.ignore("#{__dir__}/shared_tools/tools/database.rb")
13
- loader.ignore("#{__dir__}/shared_tools/tools/disk.rb")
14
- loader.ignore("#{__dir__}/shared_tools/tools/doc.rb")
15
- loader.ignore("#{__dir__}/shared_tools/tools/docker.rb")
16
- loader.ignore("#{__dir__}/shared_tools/tools/eval.rb")
17
- loader.setup
7
+
8
+ # Set up Zeitwerk loader outside module, then pass reference in
9
+ SharedToolsLoader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
10
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/ruby_llm.rb")
11
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/mcp.rb") # Documentation/loader file only
12
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/mcp") # Entire mcp directory (naming issues)
13
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/browser.rb")
14
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/computer.rb")
15
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/database.rb")
16
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/disk.rb")
17
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/doc.rb")
18
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/docker.rb")
19
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/eval.rb")
20
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/version.rb") # Defines VERSION constant, not Version class
21
+ SharedToolsLoader.ignore("#{__dir__}/shared_tools/tools/incomplete") # Empty/incomplete tools directory
22
+ SharedToolsLoader.setup
18
23
 
19
24
  module SharedTools
20
- @auto_execute ||= true # Auto-execute by default, no human-in-the-loop
21
- class << self
25
+ @auto_execute ||= true # Auto-execute by default, no human-in-the-loop
22
26
 
27
+ class << self
23
28
  def auto_execute(wildwest=true)
24
29
  @auto_execute = wildwest
25
30
  end
26
31
 
32
+ # Load all tool classes so they're available via ObjectSpace
33
+ # Call this when using AIA with --rq shared_tools
34
+ # Uses manual loading to gracefully handle missing dependencies
35
+ def load_all_tools
36
+ tools_dir = File.join(__dir__, 'shared_tools', 'tools')
37
+ Dir.glob(File.join(tools_dir, '*_tool.rb')).each do |tool_file|
38
+ begin
39
+ require tool_file
40
+ rescue LoadError => e
41
+ # Skip tools with missing dependencies
42
+ warn "SharedTools: Skipping #{File.basename(tool_file)} - #{e.message}" if ENV['DEBUG']
43
+ end
44
+ end
45
+ end
46
+
47
+ # Get all available tool classes (those that inherit from RubyLLM::Tool)
48
+ # Only returns tools that can be successfully instantiated without arguments (RubyLLM requirement)
49
+ def tools
50
+ load_all_tools
51
+ ObjectSpace.each_object(Class).select do |klass|
52
+ next false unless klass < RubyLLM::Tool
53
+ next false unless klass.to_s.start_with?('SharedTools::')
54
+
55
+ # Actually try to instantiate the tool to verify it works
56
+ # RubyLLM calls tool.new without args, so tools must be instantiable this way
57
+ begin
58
+ klass.new
59
+ true
60
+ rescue ArgumentError, LoadError, StandardError => e
61
+ # Skip tools that can't be instantiated (missing args, missing platform drivers, etc.)
62
+ warn "SharedTools: Excluding #{klass} - #{e.message}" if ENV['DEBUG']
63
+ false
64
+ end
65
+ end
66
+ end
67
+
27
68
  def execute?(tool: 'unknown', stuff: '')
28
69
  # Return true if auto_execute is explicitly enabled
29
70
  return true if @auto_execute == true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shared_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -278,11 +278,14 @@ files:
278
278
  - lib/shared_tools/tools/browser/watir_driver.rb
279
279
  - lib/shared_tools/tools/browser_tool.rb
280
280
  - lib/shared_tools/tools/calculator_tool.rb
281
+ - lib/shared_tools/tools/clipboard_tool.rb
281
282
  - lib/shared_tools/tools/composite_analysis_tool.rb
282
283
  - lib/shared_tools/tools/computer.rb
283
284
  - lib/shared_tools/tools/computer/base_driver.rb
284
285
  - lib/shared_tools/tools/computer/mac_driver.rb
285
286
  - lib/shared_tools/tools/computer_tool.rb
287
+ - lib/shared_tools/tools/cron_tool.rb
288
+ - lib/shared_tools/tools/current_date_time_tool.rb
286
289
  - lib/shared_tools/tools/data_science_kit.rb
287
290
  - lib/shared_tools/tools/database.rb
288
291
  - lib/shared_tools/tools/database/base_driver.rb
@@ -306,6 +309,7 @@ files:
306
309
  - lib/shared_tools/tools/disk/file_write_tool.rb
307
310
  - lib/shared_tools/tools/disk/local_driver.rb
308
311
  - lib/shared_tools/tools/disk_tool.rb
312
+ - lib/shared_tools/tools/dns_tool.rb
309
313
  - lib/shared_tools/tools/doc.rb
310
314
  - lib/shared_tools/tools/doc/pdf_reader_tool.rb
311
315
  - lib/shared_tools/tools/doc_tool.rb
@@ -319,6 +323,7 @@ files:
319
323
  - lib/shared_tools/tools/eval/shell_eval_tool.rb
320
324
  - lib/shared_tools/tools/eval_tool.rb
321
325
  - lib/shared_tools/tools/secure_tool_template.rb
326
+ - lib/shared_tools/tools/system_info_tool.rb
322
327
  - lib/shared_tools/tools/version.rb
323
328
  - lib/shared_tools/tools/weather_tool.rb
324
329
  - lib/shared_tools/tools/workflow_manager_tool.rb
@@ -344,7 +349,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
349
  - !ruby/object:Gem::Version
345
350
  version: '0'
346
351
  requirements: []
347
- rubygems_version: 3.7.2
352
+ rubygems_version: 4.0.1
348
353
  specification_version: 4
349
354
  summary: Shared utilities and AI tools for Ruby applications with configurable logging
350
355
  test_files: []