shared_tools 0.4.4 → 0.6.0
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 +40 -0
- data/README.md +211 -7
- data/lib/shared_tools/apply_patch_tool.rb +4 -0
- data/lib/shared_tools/bash_tool.rb +4 -0
- data/lib/shared_tools/bundle_tool.rb +4 -0
- data/lib/shared_tools/command_guard.rb +35 -0
- data/lib/shared_tools/csv_read_tool.rb +4 -0
- data/lib/shared_tools/csv_write_tool.rb +4 -0
- data/lib/shared_tools/data_path.rb +52 -0
- data/lib/shared_tools/diff_tool.rb +4 -0
- data/lib/shared_tools/download_file_tool.rb +4 -0
- data/lib/shared_tools/gem_tool.rb +4 -0
- data/lib/shared_tools/git_tool.rb +5 -0
- data/lib/shared_tools/glob_tool.rb +4 -0
- data/lib/shared_tools/http_request_tool.rb +4 -0
- data/lib/shared_tools/json_query_tool.rb +4 -0
- data/lib/shared_tools/lint_tool.rb +4 -0
- data/lib/shared_tools/mcp/brave_search_client.rb +18 -16
- data/lib/shared_tools/mcp/chart_client.rb +18 -15
- data/lib/shared_tools/mcp/client_support.rb +103 -0
- data/lib/shared_tools/mcp/github_client.rb +23 -19
- data/lib/shared_tools/mcp/hugging_face_client.rb +22 -22
- data/lib/shared_tools/mcp/memory_client.rb +20 -15
- data/lib/shared_tools/mcp/notion_client.rb +21 -19
- data/lib/shared_tools/mcp/playwright_client.rb +19 -15
- data/lib/shared_tools/mcp/sequential_thinking_client.rb +17 -14
- data/lib/shared_tools/mcp/slack_client.rb +30 -25
- data/lib/shared_tools/mcp/tavily_client.rb +21 -21
- data/lib/shared_tools/mcp.rb +32 -41
- data/lib/shared_tools/multi_edit_tool.rb +4 -0
- data/lib/shared_tools/parse_ruby_tool.rb +4 -0
- data/lib/shared_tools/process_kill_tool.rb +4 -0
- data/lib/shared_tools/process_list_tool.rb +4 -0
- data/lib/shared_tools/process_output_tool.rb +4 -0
- data/lib/shared_tools/process_registry.rb +224 -0
- data/lib/shared_tools/process_runner.rb +69 -0
- data/lib/shared_tools/process_start_tool.rb +4 -0
- data/lib/shared_tools/python_tests_tool.rb +4 -0
- data/lib/shared_tools/replace_in_files_tool.rb +4 -0
- data/lib/shared_tools/ruby_outline.rb +207 -0
- data/lib/shared_tools/run_tests_tool.rb +4 -0
- data/lib/shared_tools/text_diff.rb +80 -0
- data/lib/shared_tools/todo_write_tool.rb +4 -0
- data/lib/shared_tools/toml_parser.rb +409 -0
- data/lib/shared_tools/toml_query_tool.rb +4 -0
- data/lib/shared_tools/tools/apply_patch_tool.rb +122 -0
- data/lib/shared_tools/tools/bash_tool.rb +102 -0
- data/lib/shared_tools/tools/browser/click_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/inspect_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +1 -1
- data/lib/shared_tools/tools/browser/visit_tool.rb +1 -1
- data/lib/shared_tools/tools/browser_tool.rb +1 -1
- data/lib/shared_tools/tools/bundle_tool.rb +96 -0
- data/lib/shared_tools/tools/calculator_tool.rb +1 -1
- data/lib/shared_tools/tools/clipboard_tool.rb +1 -1
- data/lib/shared_tools/tools/composite_analysis_tool.rb +1 -1
- data/lib/shared_tools/tools/computer_tool.rb +1 -1
- data/lib/shared_tools/tools/cron_tool.rb +1 -1
- data/lib/shared_tools/tools/csv_read_tool.rb +82 -0
- data/lib/shared_tools/tools/csv_write_tool.rb +67 -0
- data/lib/shared_tools/tools/current_date_time_tool.rb +1 -1
- data/lib/shared_tools/tools/data_science_kit.rb +1 -1
- data/lib/shared_tools/tools/database_query_tool.rb +1 -1
- data/lib/shared_tools/tools/database_tool.rb +1 -1
- data/lib/shared_tools/tools/diff_tool.rb +48 -0
- data/lib/shared_tools/tools/disk/directory_create_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/directory_delete_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/directory_list_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/directory_move_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_create_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_delete_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_move_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_read_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_replace_tool.rb +1 -1
- data/lib/shared_tools/tools/disk/file_write_tool.rb +1 -1
- data/lib/shared_tools/tools/disk_tool.rb +1 -1
- data/lib/shared_tools/tools/dns_tool.rb +1 -1
- data/lib/shared_tools/tools/doc/docx_reader_tool.rb +1 -1
- data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +1 -1
- data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +1 -1
- data/lib/shared_tools/tools/doc/text_reader_tool.rb +1 -1
- data/lib/shared_tools/tools/doc_tool.rb +1 -1
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +1 -1
- data/lib/shared_tools/tools/download_file_tool.rb +87 -0
- data/lib/shared_tools/tools/enabler.rb +2 -2
- data/lib/shared_tools/tools/error_handling_tool.rb +1 -1
- data/lib/shared_tools/tools/eval/python_eval_tool.rb +1 -1
- data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +1 -1
- data/lib/shared_tools/tools/eval/shell_eval_tool.rb +1 -1
- data/lib/shared_tools/tools/eval_tool.rb +1 -1
- data/lib/shared_tools/tools/gem_tool.rb +157 -0
- data/lib/shared_tools/tools/git/add_tool.rb +73 -0
- data/lib/shared_tools/tools/git/blame_tool.rb +77 -0
- data/lib/shared_tools/tools/git/branch_tool.rb +55 -0
- data/lib/shared_tools/tools/git/checkout_tool.rb +69 -0
- data/lib/shared_tools/tools/git/commit_tool.rb +73 -0
- data/lib/shared_tools/tools/git/diff_tool.rb +69 -0
- data/lib/shared_tools/tools/git/grep_tool.rb +82 -0
- data/lib/shared_tools/tools/git/helpers.rb +70 -0
- data/lib/shared_tools/tools/git/log_tool.rb +71 -0
- data/lib/shared_tools/tools/git/show_tool.rb +66 -0
- data/lib/shared_tools/tools/git/status_tool.rb +46 -0
- data/lib/shared_tools/tools/git.rb +18 -0
- data/lib/shared_tools/tools/git_tool.rb +148 -0
- data/lib/shared_tools/tools/glob_tool.rb +75 -0
- data/lib/shared_tools/tools/http_helpers.rb +119 -0
- data/lib/shared_tools/tools/http_request_tool.rb +91 -0
- data/lib/shared_tools/tools/json_query_tool.rb +81 -0
- data/lib/shared_tools/tools/lint_tool.rb +93 -0
- data/lib/shared_tools/tools/multi_edit_tool.rb +134 -0
- data/lib/shared_tools/tools/notification_tool.rb +1 -1
- data/lib/shared_tools/tools/parse_ruby_tool.rb +127 -0
- data/lib/shared_tools/tools/process_kill_tool.rb +55 -0
- data/lib/shared_tools/tools/process_list_tool.rb +41 -0
- data/lib/shared_tools/tools/process_output_tool.rb +69 -0
- data/lib/shared_tools/tools/process_start_tool.rb +0 -0
- data/lib/shared_tools/tools/python_tests_tool.rb +105 -0
- data/lib/shared_tools/tools/replace_in_files_tool.rb +166 -0
- data/lib/shared_tools/tools/run_tests_tool.rb +108 -0
- data/lib/shared_tools/tools/search_codebase_tool.rb +178 -47
- data/lib/shared_tools/tools/secure_tool_template.rb +1 -1
- data/lib/shared_tools/tools/system_info_tool.rb +1 -1
- data/lib/shared_tools/tools/todo_write_tool.rb +81 -0
- data/lib/shared_tools/tools/toml_query_tool.rb +81 -0
- data/lib/shared_tools/tools/toolchain_helpers.rb +64 -0
- data/lib/shared_tools/tools/tree_tool.rb +100 -0
- data/lib/shared_tools/tools/weather_tool.rb +1 -1
- data/lib/shared_tools/tools/web_fetch_tool.rb +85 -0
- data/lib/shared_tools/tools/workflow_manager_tool.rb +1 -1
- data/lib/shared_tools/tools/yaml_query_tool.rb +86 -0
- data/lib/shared_tools/tree_tool.rb +4 -0
- data/lib/shared_tools/url_guard.rb +106 -0
- data/lib/shared_tools/utilities.rb +2 -2
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools/web_fetch_tool.rb +4 -0
- data/lib/shared_tools/yaml_query_tool.rb +4 -0
- data/lib/shared_tools.rb +1 -1
- metadata +516 -409
- data/lib/shared_tools/mcp/streamable_http_patch.rb +0 -42
- data/lib/shared_tools/tools/version.rb +0 -7
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../shared_tools"
|
|
4
|
+
require_relative "../process_runner"
|
|
5
|
+
require_relative "../command_guard"
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# Runs ONE allowlisted executable with arguments.
|
|
10
|
+
#
|
|
11
|
+
# Deliberately NOT a shell: there are no pipes, redirects, globs,
|
|
12
|
+
# quoting, or variable expansion. The program goes in `command`; each
|
|
13
|
+
# argument is a separate element of `args` and is passed verbatim as
|
|
14
|
+
# argv. This is the safe primitive that the OS-command-injection class of
|
|
15
|
+
# bug cannot touch, because no shell ever parses the input.
|
|
16
|
+
#
|
|
17
|
+
# Requires user authorization (see SharedTools.execute?) AND the
|
|
18
|
+
# executable being on allowed_commands (empty by default — nothing is
|
|
19
|
+
# allowed until the caller configures it).
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# tool = SharedTools::Tools::BashTool.new(allowed_commands: ["ls", "cat"])
|
|
23
|
+
# tool.execute(command: "ls", args: ["-la"])
|
|
24
|
+
class BashTool < ::RubyLLM::Tool
|
|
25
|
+
DEFAULT_TIMEOUT = 30
|
|
26
|
+
|
|
27
|
+
def self.name = 'bash'
|
|
28
|
+
|
|
29
|
+
description "Run a single ALLOWLISTED executable with arguments. " \
|
|
30
|
+
"No shell is involved: no pipes, redirects, globs, or variable expansion. " \
|
|
31
|
+
"Put the program name in `command` and each argument as its own element of `args`."
|
|
32
|
+
|
|
33
|
+
parameters do
|
|
34
|
+
string :command, description: "Executable name (must be on the allowlist). No path, no shell characters."
|
|
35
|
+
array :args, of: :string, description: "Arguments passed verbatim to the program, one per element. Optional.", required: false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param allowed_commands [Array<String>] executables permitted to run. Empty by default (nothing allowed).
|
|
39
|
+
# @param logger [Logger] optional logger
|
|
40
|
+
def initialize(allowed_commands: [], logger: nil)
|
|
41
|
+
@allowed_commands = allowed_commands
|
|
42
|
+
@logger = logger || RubyLLM.logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param command [String]
|
|
46
|
+
# @param args [Array<String>, nil]
|
|
47
|
+
#
|
|
48
|
+
# @return [String, Hash]
|
|
49
|
+
def execute(command:, args: nil)
|
|
50
|
+
@logger.info("#{self.class.name}#execute command=#{command.inspect} args=#{args.inspect}")
|
|
51
|
+
|
|
52
|
+
exe = SharedTools::CommandGuard.new(@allowed_commands).check!(command)
|
|
53
|
+
argv = sanitize_args(args)
|
|
54
|
+
|
|
55
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: ([exe] + argv).join(" "))
|
|
56
|
+
unless allowed
|
|
57
|
+
@logger.warn("User declined to run #{([exe] + argv).join(' ')}")
|
|
58
|
+
return { error: "User declined to run the command" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
out, err, status = SharedTools::ProcessRunner.capture(
|
|
62
|
+
[exe, *argv],
|
|
63
|
+
env: clean_env,
|
|
64
|
+
timeout: DEFAULT_TIMEOUT,
|
|
65
|
+
unsetenv_others: true
|
|
66
|
+
)
|
|
67
|
+
format_result(exe, argv, out, err, status)
|
|
68
|
+
rescue SharedTools::CommandGuard::Blocked => e
|
|
69
|
+
@logger.error("#{self.class.name} command denied: #{e.message}")
|
|
70
|
+
{ error: e.message }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def sanitize_args(args)
|
|
76
|
+
nul = 0.chr
|
|
77
|
+
Array(args).map do |arg|
|
|
78
|
+
str = arg.to_s
|
|
79
|
+
raise SharedTools::CommandGuard::Blocked, "argument contains a NUL byte" if str.include?(nul)
|
|
80
|
+
|
|
81
|
+
str
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def clean_env
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def format_result(exe, argv, out, err, status)
|
|
90
|
+
body = +"argv: #{([exe] + argv).inspect}\n"
|
|
91
|
+
body << if status == :timeout
|
|
92
|
+
"result: timed out after #{DEFAULT_TIMEOUT}s (killed)\n"
|
|
93
|
+
else
|
|
94
|
+
"exit: #{status.exitstatus}\n"
|
|
95
|
+
end
|
|
96
|
+
body << "\n--- stdout ---\n#{out}" unless out.empty?
|
|
97
|
+
body << "\n--- stderr ---\n#{err}" unless err.empty?
|
|
98
|
+
body
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
|
|
18
18
|
description "A browser automation tool for finding UI elements by their text content."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :text_content, description: "Search for elements containing this text"
|
|
22
22
|
string :selector, description: "Optional CSS selector to further filter results", required: false
|
|
23
23
|
integer :context_size, description: "Number of parent elements to include for context", required: false
|
|
@@ -11,7 +11,7 @@ module SharedTools
|
|
|
11
11
|
|
|
12
12
|
description "Take a screenshot of the current browser page and save it to a file."
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
parameters do
|
|
15
15
|
string :path, required: false,
|
|
16
16
|
description: "File path to save the screenshot (e.g. 'screenshot.png'). " \
|
|
17
17
|
"Defaults to a timestamped name in the current directory."
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
|
|
18
18
|
description "A browser automation tool for finding and inspecting elements by CSS selector."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :selector, description: "CSS selector to target specific elements"
|
|
22
22
|
integer :context_size, description: "Number of parent elements to include for context", required: false
|
|
23
23
|
end
|
|
@@ -12,7 +12,7 @@ module SharedTools
|
|
|
12
12
|
|
|
13
13
|
description "A browser automation tool for clicking a specific link."
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
parameters do
|
|
16
16
|
string :selector, description: "The ID / name of the text field / area to interact with."
|
|
17
17
|
string :text, description: "The text to set."
|
|
18
18
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../shared_tools"
|
|
4
|
+
require_relative "toolchain_helpers"
|
|
5
|
+
|
|
6
|
+
module SharedTools
|
|
7
|
+
module Tools
|
|
8
|
+
# Runs Bundler operations in a project. The mutating sibling of the
|
|
9
|
+
# read-only GemTool. install/update/add reach the network and change the
|
|
10
|
+
# project's gems, so this requires user authorization (see
|
|
11
|
+
# SharedTools.execute?).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# tool = SharedTools::Tools::BundleTool.new(root: "./my-project")
|
|
15
|
+
# tool.execute(action: "install")
|
|
16
|
+
# tool.execute(action: "add", gem: "rack")
|
|
17
|
+
class BundleTool < ::RubyLLM::Tool
|
|
18
|
+
include ToolchainHelpers
|
|
19
|
+
|
|
20
|
+
ACTIONS = %w[install update outdated check lock add].freeze
|
|
21
|
+
NAME_RE = /\A[A-Za-z0-9_.-]+\z/
|
|
22
|
+
DEFAULT_TIMEOUT = 180
|
|
23
|
+
|
|
24
|
+
def self.name = 'bundle'
|
|
25
|
+
|
|
26
|
+
description "Run Bundler in a project. Actions: install, update, outdated, check, lock, add. " \
|
|
27
|
+
"For 'add' provide a gem name; for 'update' a gem name is optional (omit to update " \
|
|
28
|
+
"all). Requires a Gemfile."
|
|
29
|
+
|
|
30
|
+
parameters do
|
|
31
|
+
string :action, description: "One of: install, update, outdated, check, lock, add."
|
|
32
|
+
string :gem, description: "Gem name — required for 'add', optional for 'update'.", required: false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param root [String] optional, defaults to the current directory
|
|
36
|
+
# @param logger [Logger] optional logger
|
|
37
|
+
def initialize(root: nil, logger: nil)
|
|
38
|
+
@root = root || Dir.pwd
|
|
39
|
+
@logger = logger || RubyLLM.logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param action [String]
|
|
43
|
+
# @param gem [String, nil]
|
|
44
|
+
#
|
|
45
|
+
# @return [String, Hash]
|
|
46
|
+
def execute(action:, gem: nil)
|
|
47
|
+
@logger.info("#{self.class.name}#execute action=#{action} gem=#{gem.inspect}")
|
|
48
|
+
|
|
49
|
+
act = action.to_s.strip.downcase
|
|
50
|
+
return { error: "unknown action: #{act} (use #{ACTIONS.join(', ')})" } unless ACTIONS.include?(act)
|
|
51
|
+
return { error: "a Gemfile is required (none at #{@root})" } unless gemfile?
|
|
52
|
+
|
|
53
|
+
args = build_args(act, gem)
|
|
54
|
+
return args if args.is_a?(Hash) # validation error
|
|
55
|
+
|
|
56
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: args.join(" "))
|
|
57
|
+
unless allowed
|
|
58
|
+
@logger.warn("User declined to run #{args.join(' ')}")
|
|
59
|
+
return { error: "User declined to run #{args.join(' ')}" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
out, err, status = run_in_project(args, use_bundle: false, timeout: DEFAULT_TIMEOUT)
|
|
63
|
+
toolchain_output(out, err, status,
|
|
64
|
+
pass_label: "bundle #{act}: ok",
|
|
65
|
+
fail_label: "bundle #{act}: failed",
|
|
66
|
+
timeout: DEFAULT_TIMEOUT)
|
|
67
|
+
rescue CommandMissing
|
|
68
|
+
@logger.error("#{self.class.name}: bundler is not available")
|
|
69
|
+
{ error: "bundler is not available (gem install bundler)" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_args(act, gem_name)
|
|
75
|
+
case act
|
|
76
|
+
when "add"
|
|
77
|
+
name = gem_name.to_s.strip
|
|
78
|
+
return { error: "'add' requires a valid gem name" } unless name.match?(NAME_RE)
|
|
79
|
+
|
|
80
|
+
["bundle", "add", name]
|
|
81
|
+
when "update"
|
|
82
|
+
name = gem_name.to_s.strip
|
|
83
|
+
if name.empty?
|
|
84
|
+
["bundle", "update"]
|
|
85
|
+
else
|
|
86
|
+
return { error: "invalid gem name: #{name.inspect}" } unless name.match?(NAME_RE)
|
|
87
|
+
|
|
88
|
+
["bundle", "update", name]
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
["bundle", act]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -44,7 +44,7 @@ module SharedTools
|
|
|
44
44
|
puts "Result: #{result[:result]}" # 3.3333
|
|
45
45
|
DESCRIPTION
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
parameters do
|
|
48
48
|
string :expression, description: <<~DESC.strip
|
|
49
49
|
Mathematical expression to evaluate using standard arithmetic operators and parentheses.
|
|
50
50
|
Supported operations include: addition (+), subtraction (-), multiplication (*),
|
|
@@ -25,7 +25,7 @@ module SharedTools
|
|
|
25
25
|
- 'clear' — Empty the clipboard
|
|
26
26
|
DESC
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
parameters do
|
|
29
29
|
string :action, description: "Action to perform: 'read', 'write', or 'clear'"
|
|
30
30
|
string :text, required: false, description: "Text to write. Required only for the 'write' action."
|
|
31
31
|
end
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
from initial data loading through final insights. Handles CSV, JSON, and text data formats.
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :data_source, description: <<~DESC.strip, required: false
|
|
22
22
|
Primary data source to analyze. Can be either a local file path or a web URL.
|
|
23
23
|
For files: Use relative or absolute paths to CSV, JSON, XML, or text files.
|
|
@@ -33,7 +33,7 @@ module SharedTools
|
|
|
33
33
|
'every weekday', 'first day of every month at midnight'
|
|
34
34
|
DESC
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
parameters do
|
|
37
37
|
string :action, description: "Action: 'parse', 'validate', 'next_times', 'generate'"
|
|
38
38
|
string :expression, required: false, description: "5-field cron expression. Required for parse, validate, next_times."
|
|
39
39
|
integer :count, required: false, description: "Number of next execution times. Default: 5."
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require_relative '../../shared_tools'
|
|
5
|
+
|
|
6
|
+
module SharedTools
|
|
7
|
+
module Tools
|
|
8
|
+
# Reads a CSV file and returns its rows in a compact, readable form.
|
|
9
|
+
# Read-only.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# tool = SharedTools::Tools::CsvReadTool.new
|
|
13
|
+
# tool.execute(path: "./sales.csv")
|
|
14
|
+
# tool.execute(path: "./sales.csv", headers: false, limit: 10)
|
|
15
|
+
class CsvReadTool < ::RubyLLM::Tool
|
|
16
|
+
MAX_BYTES = 10 * 1024 * 1024
|
|
17
|
+
DEFAULT_LIMIT = 100
|
|
18
|
+
|
|
19
|
+
def self.name = 'csv_read'
|
|
20
|
+
|
|
21
|
+
description "Read a CSV file and return its rows. By default the first row is treated as a " \
|
|
22
|
+
"header. Use limit to cap the number of data rows returned."
|
|
23
|
+
|
|
24
|
+
parameters do
|
|
25
|
+
string :path, description: "CSV file to read."
|
|
26
|
+
boolean :headers, description: "Treat the first row as column headers. Default true.", required: false
|
|
27
|
+
integer :limit, description: "Maximum number of data rows to return (default #{DEFAULT_LIMIT}).", required: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param logger [Logger] optional logger
|
|
31
|
+
def initialize(logger: nil)
|
|
32
|
+
@logger = logger || RubyLLM.logger
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param path [String]
|
|
36
|
+
# @param headers [Boolean]
|
|
37
|
+
# @param limit [Integer]
|
|
38
|
+
#
|
|
39
|
+
# @return [String, Hash]
|
|
40
|
+
def execute(path:, headers: true, limit: DEFAULT_LIMIT)
|
|
41
|
+
@logger.info("#{self.class.name}#execute path=#{path.inspect} headers=#{headers} limit=#{limit}")
|
|
42
|
+
|
|
43
|
+
return { error: "file not found: #{path}" } unless File.exist?(path)
|
|
44
|
+
return { error: "not a file: #{path}" } unless File.file?(path)
|
|
45
|
+
return { error: "file too large (> #{MAX_BYTES} bytes)" } if File.size(path) > MAX_BYTES
|
|
46
|
+
|
|
47
|
+
rows = CSV.read(path)
|
|
48
|
+
return "empty CSV" if rows.empty?
|
|
49
|
+
|
|
50
|
+
max = limit.to_i
|
|
51
|
+
max = DEFAULT_LIMIT if max <= 0
|
|
52
|
+
render(rows, headers, max)
|
|
53
|
+
rescue CSV::MalformedCSVError => e
|
|
54
|
+
@logger.error("#{self.class.name} malformed CSV: #{e.message}")
|
|
55
|
+
{ error: "malformed CSV: #{e.message}" }
|
|
56
|
+
rescue => e
|
|
57
|
+
@logger.error("#{self.class.name} failed: #{e.message}")
|
|
58
|
+
{ error: e.message }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def render(rows, headers, max)
|
|
64
|
+
header = headers ? rows.first : nil
|
|
65
|
+
data = headers ? rows[1..] : rows
|
|
66
|
+
shown = data.first(max)
|
|
67
|
+
|
|
68
|
+
lines = []
|
|
69
|
+
lines << "columns: #{header.join(' | ')}" if header
|
|
70
|
+
lines << "#{data.size} row#{data.size == 1 ? '' : 's'}#{data.size > max ? " (showing #{max})" : ''}"
|
|
71
|
+
shown.each_with_index do |row, i|
|
|
72
|
+
lines << if header
|
|
73
|
+
"#{i + 1}. " + header.zip(row).map { |k, v| "#{k}=#{v}" }.join(", ")
|
|
74
|
+
else
|
|
75
|
+
"#{i + 1}. " + row.join(" | ")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
lines.join("\n")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative '../../shared_tools'
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# Writes rows to a CSV file. Optional header row. Missing parent
|
|
10
|
+
# directories are created. Overwrites an existing file. Mutating —
|
|
11
|
+
# requires user authorization (see SharedTools.execute?).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# tool = SharedTools::Tools::CsvWriteTool.new
|
|
15
|
+
# tool.execute(path: "./out.csv", headers: ["name", "age"], rows: [["Alice", 30]])
|
|
16
|
+
class CsvWriteTool < ::RubyLLM::Tool
|
|
17
|
+
def self.name = 'csv_write'
|
|
18
|
+
|
|
19
|
+
description "Write rows to a CSV file. Provide rows as an array of arrays (each inner array " \
|
|
20
|
+
"is a row of cell values, written as strings), with an optional headers array " \
|
|
21
|
+
"written as the first line. Overwrites an existing file."
|
|
22
|
+
|
|
23
|
+
parameters do
|
|
24
|
+
string :path, description: "Destination CSV path."
|
|
25
|
+
array :rows, description: "Array of rows; each row is an array of cell values (as strings)." do
|
|
26
|
+
array of: :string
|
|
27
|
+
end
|
|
28
|
+
array :headers, of: :string, description: "Optional column header row, written first.", required: false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param logger [Logger] optional logger
|
|
32
|
+
def initialize(logger: nil)
|
|
33
|
+
@logger = logger || RubyLLM.logger
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param path [String]
|
|
37
|
+
# @param rows [Array<Array>]
|
|
38
|
+
# @param headers [Array<String>, nil]
|
|
39
|
+
#
|
|
40
|
+
# @return [String, Hash]
|
|
41
|
+
def execute(path:, rows:, headers: nil)
|
|
42
|
+
@logger.info("#{self.class.name}#execute path=#{path.inspect} rows=#{Array(rows).size}")
|
|
43
|
+
|
|
44
|
+
return { error: "rows must be an array of arrays" } unless rows.is_a?(Array)
|
|
45
|
+
return { error: "path is a directory: #{path}" } if File.directory?(path)
|
|
46
|
+
|
|
47
|
+
all = []
|
|
48
|
+
all << Array(headers) if headers && !Array(headers).empty?
|
|
49
|
+
rows.each { |row| all << Array(row) }
|
|
50
|
+
|
|
51
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: "Write #{rows.size} row(s) to #{path}")
|
|
52
|
+
unless allowed
|
|
53
|
+
@logger.warn("User declined to write CSV to #{path}")
|
|
54
|
+
return { error: "User declined to write the CSV file" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
58
|
+
CSV.open(path, "w") { |csv| all.each { |row| csv << row } }
|
|
59
|
+
|
|
60
|
+
"Wrote #{rows.size} row#{rows.size == 1 ? '' : 's'} to #{path}"
|
|
61
|
+
rescue => e
|
|
62
|
+
@logger.error("#{self.class.name} failed: #{e.message}")
|
|
63
|
+
{ error: e.message }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
recommendations and statistical significance testing where applicable.
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :analysis_type, description: <<~DESC.strip, required: true
|
|
22
22
|
Type of data science analysis to perform:
|
|
23
23
|
- 'statistical_summary': Descriptive statistics, distributions, outlier detection
|
|
@@ -41,7 +41,7 @@ module SharedTools
|
|
|
41
41
|
puts "Found #{result[:row_count]} users"
|
|
42
42
|
DESCRIPTION
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
parameters do
|
|
45
45
|
string :query, description: <<~DESC.strip
|
|
46
46
|
SQL SELECT query to execute against the database. Only SELECT statements are permitted
|
|
47
47
|
for security reasons - INSERT, UPDATE, DELETE, and DDL statements will be rejected.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../shared_tools'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
# Compares two blocks of text and returns a readable line diff. In-process,
|
|
8
|
+
# read-only — no filesystem or git access. For diffing files already on
|
|
9
|
+
# disk, see Git::DiffTool.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# tool = SharedTools::Tools::DiffTool.new
|
|
13
|
+
# tool.execute(old: "a\nb\n", new: "a\nc\n")
|
|
14
|
+
class DiffTool < ::RubyLLM::Tool
|
|
15
|
+
def self.name = 'diff'
|
|
16
|
+
|
|
17
|
+
description "Compare two blocks of text and return a readable line-by-line diff " \
|
|
18
|
+
"(removed lines prefixed -, added lines prefixed +). In-process."
|
|
19
|
+
|
|
20
|
+
parameters do
|
|
21
|
+
string :old, description: "The original text."
|
|
22
|
+
string :new, description: "The changed text."
|
|
23
|
+
string :old_label, description: "Label for the original side. Default 'old'.", required: false
|
|
24
|
+
string :new_label, description: "Label for the changed side. Default 'new'.", required: false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param logger [Logger] optional logger
|
|
28
|
+
def initialize(logger: nil)
|
|
29
|
+
@logger = logger || RubyLLM.logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param old [String]
|
|
33
|
+
# @param new [String]
|
|
34
|
+
# @param old_label [String]
|
|
35
|
+
# @param new_label [String]
|
|
36
|
+
#
|
|
37
|
+
# @return [String]
|
|
38
|
+
def execute(old:, new:, old_label: "old", new_label: "new")
|
|
39
|
+
@logger.info("#{self.class.name}#execute old_label=#{old_label} new_label=#{new_label}")
|
|
40
|
+
|
|
41
|
+
SharedTools::TextDiff.unified(old.to_s, new.to_s, old_label: old_label.to_s, new_label: new_label.to_s)
|
|
42
|
+
rescue => e
|
|
43
|
+
@logger.error("#{self.class.name} failed: #{e.message}")
|
|
44
|
+
{ error: e.message }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
|
|
18
18
|
description "Replaces a specific string in a file (old_text => new_text)."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :old_text, description: "the old text (e.g. `puts 'ABC'`)"
|
|
22
22
|
string :new_text, description: "the new text (e.g. `puts 'DEF'`)"
|
|
23
23
|
string :path, description: "a path (e.g. `./main.rb`)"
|