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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/shared_tools/tools/browser_tool.rb +8 -2
- data/lib/shared_tools/tools/clipboard_tool.rb +194 -0
- data/lib/shared_tools/tools/computer_tool.rb +8 -2
- data/lib/shared_tools/tools/cron_tool.rb +474 -0
- data/lib/shared_tools/tools/current_date_time_tool.rb +154 -0
- data/lib/shared_tools/tools/database_tool.rb +8 -3
- data/lib/shared_tools/tools/dns_tool.rb +356 -0
- data/lib/shared_tools/tools/system_info_tool.rb +417 -0
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools.rb +54 -13
- metadata +7 -2
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm/tool'
|
|
4
|
+
require 'resolv'
|
|
5
|
+
require 'socket'
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# A tool for performing DNS lookups and reverse DNS queries.
|
|
10
|
+
# Uses Ruby's built-in Resolv library for cross-platform support.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# tool = SharedTools::Tools::DnsTool.new
|
|
14
|
+
# result = tool.execute(action: 'lookup', hostname: 'google.com')
|
|
15
|
+
# puts result[:addresses]
|
|
16
|
+
class DnsTool < RubyLLM::Tool
|
|
17
|
+
def self.name = 'dns'
|
|
18
|
+
|
|
19
|
+
description <<~'DESCRIPTION'
|
|
20
|
+
Perform DNS lookups and reverse DNS queries.
|
|
21
|
+
|
|
22
|
+
Uses Ruby's built-in Resolv library for cross-platform DNS resolution.
|
|
23
|
+
|
|
24
|
+
Actions:
|
|
25
|
+
- 'lookup': Resolve a hostname to IP addresses
|
|
26
|
+
- 'reverse': Perform reverse DNS lookup (IP to hostname)
|
|
27
|
+
- 'mx': Get MX (mail exchange) records for a domain
|
|
28
|
+
- 'txt': Get TXT records for a domain
|
|
29
|
+
- 'ns': Get NS (nameserver) records for a domain
|
|
30
|
+
- 'all': Get all available DNS records for a domain
|
|
31
|
+
|
|
32
|
+
Record Types (for lookup action):
|
|
33
|
+
- 'A': IPv4 addresses (default)
|
|
34
|
+
- 'AAAA': IPv6 addresses
|
|
35
|
+
- 'CNAME': Canonical name records
|
|
36
|
+
- 'ANY': All record types
|
|
37
|
+
|
|
38
|
+
Example usage:
|
|
39
|
+
tool = SharedTools::Tools::DnsTool.new
|
|
40
|
+
|
|
41
|
+
# Basic lookup
|
|
42
|
+
tool.execute(action: 'lookup', hostname: 'google.com')
|
|
43
|
+
|
|
44
|
+
# Get MX records
|
|
45
|
+
tool.execute(action: 'mx', hostname: 'gmail.com')
|
|
46
|
+
|
|
47
|
+
# Reverse lookup
|
|
48
|
+
tool.execute(action: 'reverse', ip: '8.8.8.8')
|
|
49
|
+
|
|
50
|
+
# Get all records
|
|
51
|
+
tool.execute(action: 'all', hostname: 'example.com')
|
|
52
|
+
DESCRIPTION
|
|
53
|
+
|
|
54
|
+
params do
|
|
55
|
+
string :action, description: <<~DESC.strip
|
|
56
|
+
The DNS action to perform:
|
|
57
|
+
- 'lookup': Resolve hostname to IP addresses
|
|
58
|
+
- 'reverse': IP to hostname lookup
|
|
59
|
+
- 'mx': Get mail exchange records
|
|
60
|
+
- 'txt': Get TXT records
|
|
61
|
+
- 'ns': Get nameserver records
|
|
62
|
+
- 'all': Get all available records
|
|
63
|
+
DESC
|
|
64
|
+
|
|
65
|
+
string :hostname, description: <<~DESC.strip, required: false
|
|
66
|
+
The hostname/domain to look up.
|
|
67
|
+
Required for 'lookup', 'mx', 'txt', 'ns', and 'all' actions.
|
|
68
|
+
DESC
|
|
69
|
+
|
|
70
|
+
string :ip, description: <<~DESC.strip, required: false
|
|
71
|
+
The IP address for reverse DNS lookup.
|
|
72
|
+
Required for 'reverse' action.
|
|
73
|
+
DESC
|
|
74
|
+
|
|
75
|
+
string :record_type, description: <<~DESC.strip, required: false
|
|
76
|
+
Record type for 'lookup' action:
|
|
77
|
+
- 'A': IPv4 addresses (default)
|
|
78
|
+
- 'AAAA': IPv6 addresses
|
|
79
|
+
- 'CNAME': Canonical name
|
|
80
|
+
- 'ANY': All types
|
|
81
|
+
DESC
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param logger [Logger] optional logger
|
|
85
|
+
def initialize(logger: nil)
|
|
86
|
+
@logger = logger || RubyLLM.logger
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Execute DNS action
|
|
90
|
+
#
|
|
91
|
+
# @param action [String] DNS action to perform
|
|
92
|
+
# @param hostname [String, nil] hostname to look up
|
|
93
|
+
# @param ip [String, nil] IP for reverse lookup
|
|
94
|
+
# @param record_type [String, nil] record type for lookup
|
|
95
|
+
# @return [Hash] DNS results
|
|
96
|
+
def execute(action:, hostname: nil, ip: nil, record_type: nil)
|
|
97
|
+
@logger.info("DnsTool#execute action=#{action.inspect}")
|
|
98
|
+
|
|
99
|
+
case action.to_s.downcase
|
|
100
|
+
when 'lookup'
|
|
101
|
+
lookup(hostname, record_type || 'A')
|
|
102
|
+
when 'reverse'
|
|
103
|
+
reverse_lookup(ip)
|
|
104
|
+
when 'mx'
|
|
105
|
+
mx_lookup(hostname)
|
|
106
|
+
when 'txt'
|
|
107
|
+
txt_lookup(hostname)
|
|
108
|
+
when 'ns'
|
|
109
|
+
ns_lookup(hostname)
|
|
110
|
+
when 'all'
|
|
111
|
+
all_records(hostname)
|
|
112
|
+
else
|
|
113
|
+
{
|
|
114
|
+
success: false,
|
|
115
|
+
error: "Unknown action: #{action}. Valid actions are: lookup, reverse, mx, txt, ns, all"
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
rescue Resolv::ResolvError => e
|
|
119
|
+
@logger.error("DnsTool DNS error: #{e.message}")
|
|
120
|
+
{
|
|
121
|
+
success: false,
|
|
122
|
+
error: "DNS resolution failed: #{e.message}"
|
|
123
|
+
}
|
|
124
|
+
rescue => e
|
|
125
|
+
@logger.error("DnsTool error: #{e.message}")
|
|
126
|
+
{
|
|
127
|
+
success: false,
|
|
128
|
+
error: e.message
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def lookup(hostname, record_type)
|
|
135
|
+
return { success: false, error: "Hostname is required" } if hostname.nil? || hostname.empty?
|
|
136
|
+
|
|
137
|
+
results = {
|
|
138
|
+
success: true,
|
|
139
|
+
hostname: hostname,
|
|
140
|
+
record_type: record_type.upcase,
|
|
141
|
+
addresses: []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resolver = Resolv::DNS.new
|
|
145
|
+
|
|
146
|
+
case record_type.upcase
|
|
147
|
+
when 'A'
|
|
148
|
+
addresses = resolver.getresources(hostname, Resolv::DNS::Resource::IN::A)
|
|
149
|
+
results[:addresses] = addresses.map { |a| { type: 'A', address: a.address.to_s } }
|
|
150
|
+
when 'AAAA'
|
|
151
|
+
addresses = resolver.getresources(hostname, Resolv::DNS::Resource::IN::AAAA)
|
|
152
|
+
results[:addresses] = addresses.map { |a| { type: 'AAAA', address: a.address.to_s } }
|
|
153
|
+
when 'CNAME'
|
|
154
|
+
cnames = resolver.getresources(hostname, Resolv::DNS::Resource::IN::CNAME)
|
|
155
|
+
results[:addresses] = cnames.map { |c| { type: 'CNAME', name: c.name.to_s } }
|
|
156
|
+
when 'ANY'
|
|
157
|
+
# Get A records
|
|
158
|
+
a_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::A)
|
|
159
|
+
results[:addresses] += a_records.map { |a| { type: 'A', address: a.address.to_s } }
|
|
160
|
+
|
|
161
|
+
# Get AAAA records
|
|
162
|
+
aaaa_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::AAAA)
|
|
163
|
+
results[:addresses] += aaaa_records.map { |a| { type: 'AAAA', address: a.address.to_s } }
|
|
164
|
+
|
|
165
|
+
# Get CNAME records
|
|
166
|
+
cname_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::CNAME)
|
|
167
|
+
results[:addresses] += cname_records.map { |c| { type: 'CNAME', name: c.name.to_s } }
|
|
168
|
+
else
|
|
169
|
+
return { success: false, error: "Unknown record type: #{record_type}. Valid types are: A, AAAA, CNAME, ANY" }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
resolver.close
|
|
173
|
+
results
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def reverse_lookup(ip)
|
|
177
|
+
return { success: false, error: "IP address is required" } if ip.nil? || ip.empty?
|
|
178
|
+
|
|
179
|
+
# Validate IP address format
|
|
180
|
+
unless valid_ip?(ip)
|
|
181
|
+
return { success: false, error: "Invalid IP address format: #{ip}" }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
hostnames = []
|
|
185
|
+
|
|
186
|
+
begin
|
|
187
|
+
# Try using Resolv for reverse lookup
|
|
188
|
+
name = Resolv.getname(ip)
|
|
189
|
+
hostnames << name
|
|
190
|
+
rescue Resolv::ResolvError
|
|
191
|
+
# Try alternative method using DNS PTR record
|
|
192
|
+
begin
|
|
193
|
+
resolver = Resolv::DNS.new
|
|
194
|
+
ptr_name = ip_to_ptr(ip)
|
|
195
|
+
ptrs = resolver.getresources(ptr_name, Resolv::DNS::Resource::IN::PTR)
|
|
196
|
+
hostnames = ptrs.map { |p| p.name.to_s }
|
|
197
|
+
resolver.close
|
|
198
|
+
rescue
|
|
199
|
+
# No reverse DNS record found
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
success: true,
|
|
205
|
+
ip: ip,
|
|
206
|
+
hostnames: hostnames,
|
|
207
|
+
found: !hostnames.empty?
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def mx_lookup(hostname)
|
|
212
|
+
return { success: false, error: "Hostname is required" } if hostname.nil? || hostname.empty?
|
|
213
|
+
|
|
214
|
+
resolver = Resolv::DNS.new
|
|
215
|
+
mx_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::MX)
|
|
216
|
+
|
|
217
|
+
records = mx_records.map do |mx|
|
|
218
|
+
{
|
|
219
|
+
priority: mx.preference,
|
|
220
|
+
exchange: mx.exchange.to_s
|
|
221
|
+
}
|
|
222
|
+
end.sort_by { |r| r[:priority] }
|
|
223
|
+
|
|
224
|
+
resolver.close
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
success: true,
|
|
228
|
+
hostname: hostname,
|
|
229
|
+
mx_records: records,
|
|
230
|
+
count: records.length
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def txt_lookup(hostname)
|
|
235
|
+
return { success: false, error: "Hostname is required" } if hostname.nil? || hostname.empty?
|
|
236
|
+
|
|
237
|
+
resolver = Resolv::DNS.new
|
|
238
|
+
txt_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::TXT)
|
|
239
|
+
|
|
240
|
+
records = txt_records.map do |txt|
|
|
241
|
+
txt.strings.join
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
resolver.close
|
|
245
|
+
|
|
246
|
+
{
|
|
247
|
+
success: true,
|
|
248
|
+
hostname: hostname,
|
|
249
|
+
txt_records: records,
|
|
250
|
+
count: records.length
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def ns_lookup(hostname)
|
|
255
|
+
return { success: false, error: "Hostname is required" } if hostname.nil? || hostname.empty?
|
|
256
|
+
|
|
257
|
+
resolver = Resolv::DNS.new
|
|
258
|
+
ns_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::NS)
|
|
259
|
+
|
|
260
|
+
records = ns_records.map { |ns| ns.name.to_s }
|
|
261
|
+
|
|
262
|
+
resolver.close
|
|
263
|
+
|
|
264
|
+
{
|
|
265
|
+
success: true,
|
|
266
|
+
hostname: hostname,
|
|
267
|
+
nameservers: records,
|
|
268
|
+
count: records.length
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def all_records(hostname)
|
|
273
|
+
return { success: false, error: "Hostname is required" } if hostname.nil? || hostname.empty?
|
|
274
|
+
|
|
275
|
+
resolver = Resolv::DNS.new
|
|
276
|
+
|
|
277
|
+
results = {
|
|
278
|
+
success: true,
|
|
279
|
+
hostname: hostname,
|
|
280
|
+
records: {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# A records
|
|
284
|
+
a_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::A)
|
|
285
|
+
results[:records][:A] = a_records.map { |a| a.address.to_s } unless a_records.empty?
|
|
286
|
+
|
|
287
|
+
# AAAA records
|
|
288
|
+
aaaa_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::AAAA)
|
|
289
|
+
results[:records][:AAAA] = aaaa_records.map { |a| a.address.to_s } unless aaaa_records.empty?
|
|
290
|
+
|
|
291
|
+
# MX records
|
|
292
|
+
mx_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::MX)
|
|
293
|
+
unless mx_records.empty?
|
|
294
|
+
results[:records][:MX] = mx_records.map do |mx|
|
|
295
|
+
{ priority: mx.preference, exchange: mx.exchange.to_s }
|
|
296
|
+
end.sort_by { |r| r[:priority] }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# TXT records
|
|
300
|
+
txt_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::TXT)
|
|
301
|
+
results[:records][:TXT] = txt_records.map { |t| t.strings.join } unless txt_records.empty?
|
|
302
|
+
|
|
303
|
+
# NS records
|
|
304
|
+
ns_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::NS)
|
|
305
|
+
results[:records][:NS] = ns_records.map { |ns| ns.name.to_s } unless ns_records.empty?
|
|
306
|
+
|
|
307
|
+
# CNAME records
|
|
308
|
+
cname_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::CNAME)
|
|
309
|
+
results[:records][:CNAME] = cname_records.map { |c| c.name.to_s } unless cname_records.empty?
|
|
310
|
+
|
|
311
|
+
# SOA record
|
|
312
|
+
begin
|
|
313
|
+
soa_records = resolver.getresources(hostname, Resolv::DNS::Resource::IN::SOA)
|
|
314
|
+
unless soa_records.empty?
|
|
315
|
+
soa = soa_records.first
|
|
316
|
+
results[:records][:SOA] = {
|
|
317
|
+
mname: soa.mname.to_s,
|
|
318
|
+
rname: soa.rname.to_s,
|
|
319
|
+
serial: soa.serial,
|
|
320
|
+
refresh: soa.refresh,
|
|
321
|
+
retry: soa.retry,
|
|
322
|
+
expire: soa.expire,
|
|
323
|
+
minimum: soa.minimum
|
|
324
|
+
}
|
|
325
|
+
end
|
|
326
|
+
rescue
|
|
327
|
+
# SOA might not be available for all domains
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
resolver.close
|
|
331
|
+
results
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def valid_ip?(ip)
|
|
335
|
+
# Check IPv4
|
|
336
|
+
return true if ip =~ /\A(\d{1,3}\.){3}\d{1,3}\z/ &&
|
|
337
|
+
ip.split('.').all? { |octet| octet.to_i.between?(0, 255) }
|
|
338
|
+
|
|
339
|
+
# Check IPv6 (simplified check)
|
|
340
|
+
return true if ip =~ /\A[\da-fA-F:]+\z/ && ip.include?(':')
|
|
341
|
+
|
|
342
|
+
false
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def ip_to_ptr(ip)
|
|
346
|
+
if ip.include?(':')
|
|
347
|
+
# IPv6 - not fully implemented, but basic support
|
|
348
|
+
ip.gsub(':', '').chars.reverse.join('.') + '.ip6.arpa'
|
|
349
|
+
else
|
|
350
|
+
# IPv4
|
|
351
|
+
ip.split('.').reverse.join('.') + '.in-addr.arpa'
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|