shared_tools 0.4.2 → 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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +211 -7
  4. data/lib/shared_tools/apply_patch_tool.rb +4 -0
  5. data/lib/shared_tools/bash_tool.rb +4 -0
  6. data/lib/shared_tools/bundle_tool.rb +4 -0
  7. data/lib/shared_tools/command_guard.rb +35 -0
  8. data/lib/shared_tools/csv_read_tool.rb +4 -0
  9. data/lib/shared_tools/csv_write_tool.rb +4 -0
  10. data/lib/shared_tools/data_path.rb +52 -0
  11. data/lib/shared_tools/diff_tool.rb +4 -0
  12. data/lib/shared_tools/download_file_tool.rb +4 -0
  13. data/lib/shared_tools/gem_tool.rb +4 -0
  14. data/lib/shared_tools/git_tool.rb +5 -0
  15. data/lib/shared_tools/glob_tool.rb +4 -0
  16. data/lib/shared_tools/http_request_tool.rb +4 -0
  17. data/lib/shared_tools/json_query_tool.rb +4 -0
  18. data/lib/shared_tools/lint_tool.rb +4 -0
  19. data/lib/shared_tools/mcp/brave_search_client.rb +18 -16
  20. data/lib/shared_tools/mcp/chart_client.rb +18 -15
  21. data/lib/shared_tools/mcp/client_support.rb +103 -0
  22. data/lib/shared_tools/mcp/github_client.rb +23 -19
  23. data/lib/shared_tools/mcp/hugging_face_client.rb +22 -22
  24. data/lib/shared_tools/mcp/memory_client.rb +20 -15
  25. data/lib/shared_tools/mcp/notion_client.rb +21 -19
  26. data/lib/shared_tools/mcp/playwright_client.rb +19 -15
  27. data/lib/shared_tools/mcp/sequential_thinking_client.rb +17 -14
  28. data/lib/shared_tools/mcp/slack_client.rb +30 -25
  29. data/lib/shared_tools/mcp/tavily_client.rb +21 -21
  30. data/lib/shared_tools/mcp.rb +32 -41
  31. data/lib/shared_tools/multi_edit_tool.rb +4 -0
  32. data/lib/shared_tools/parse_ruby_tool.rb +4 -0
  33. data/lib/shared_tools/process_kill_tool.rb +4 -0
  34. data/lib/shared_tools/process_list_tool.rb +4 -0
  35. data/lib/shared_tools/process_output_tool.rb +4 -0
  36. data/lib/shared_tools/process_registry.rb +224 -0
  37. data/lib/shared_tools/process_runner.rb +69 -0
  38. data/lib/shared_tools/process_start_tool.rb +4 -0
  39. data/lib/shared_tools/python_tests_tool.rb +4 -0
  40. data/lib/shared_tools/replace_in_files_tool.rb +4 -0
  41. data/lib/shared_tools/ruby_outline.rb +207 -0
  42. data/lib/shared_tools/run_tests_tool.rb +4 -0
  43. data/lib/shared_tools/text_diff.rb +80 -0
  44. data/lib/shared_tools/todo_write_tool.rb +4 -0
  45. data/lib/shared_tools/toml_parser.rb +409 -0
  46. data/lib/shared_tools/toml_query_tool.rb +4 -0
  47. data/lib/shared_tools/tools/apply_patch_tool.rb +122 -0
  48. data/lib/shared_tools/tools/bash_tool.rb +102 -0
  49. data/lib/shared_tools/tools/browser/click_tool.rb +1 -1
  50. data/lib/shared_tools/tools/browser/inspect_tool.rb +1 -1
  51. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +1 -1
  52. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +1 -1
  53. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +1 -1
  54. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +1 -1
  55. data/lib/shared_tools/tools/browser/visit_tool.rb +1 -1
  56. data/lib/shared_tools/tools/browser_tool.rb +1 -1
  57. data/lib/shared_tools/tools/bundle_tool.rb +96 -0
  58. data/lib/shared_tools/tools/calculator_tool.rb +12 -2
  59. data/lib/shared_tools/tools/clipboard_tool.rb +1 -1
  60. data/lib/shared_tools/tools/composite_analysis_tool.rb +1 -1
  61. data/lib/shared_tools/tools/computer_tool.rb +1 -1
  62. data/lib/shared_tools/tools/cron_tool.rb +1 -1
  63. data/lib/shared_tools/tools/csv_read_tool.rb +82 -0
  64. data/lib/shared_tools/tools/csv_write_tool.rb +67 -0
  65. data/lib/shared_tools/tools/current_date_time_tool.rb +1 -1
  66. data/lib/shared_tools/tools/data_science_kit.rb +1 -1
  67. data/lib/shared_tools/tools/database_query_tool.rb +12 -2
  68. data/lib/shared_tools/tools/database_tool.rb +1 -1
  69. data/lib/shared_tools/tools/diff_tool.rb +48 -0
  70. data/lib/shared_tools/tools/disk/directory_create_tool.rb +1 -1
  71. data/lib/shared_tools/tools/disk/directory_delete_tool.rb +1 -1
  72. data/lib/shared_tools/tools/disk/directory_list_tool.rb +1 -1
  73. data/lib/shared_tools/tools/disk/directory_move_tool.rb +1 -1
  74. data/lib/shared_tools/tools/disk/file_create_tool.rb +1 -1
  75. data/lib/shared_tools/tools/disk/file_delete_tool.rb +1 -1
  76. data/lib/shared_tools/tools/disk/file_move_tool.rb +1 -1
  77. data/lib/shared_tools/tools/disk/file_read_tool.rb +1 -1
  78. data/lib/shared_tools/tools/disk/file_replace_tool.rb +1 -1
  79. data/lib/shared_tools/tools/disk/file_write_tool.rb +1 -1
  80. data/lib/shared_tools/tools/disk_tool.rb +1 -1
  81. data/lib/shared_tools/tools/dns_tool.rb +1 -1
  82. data/lib/shared_tools/tools/doc/docx_reader_tool.rb +1 -1
  83. data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +1 -1
  84. data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +1 -1
  85. data/lib/shared_tools/tools/doc/text_reader_tool.rb +1 -1
  86. data/lib/shared_tools/tools/doc_tool.rb +1 -1
  87. data/lib/shared_tools/tools/docker/compose_run_tool.rb +1 -1
  88. data/lib/shared_tools/tools/download_file_tool.rb +87 -0
  89. data/lib/shared_tools/tools/enabler.rb +2 -2
  90. data/lib/shared_tools/tools/error_handling_tool.rb +1 -1
  91. data/lib/shared_tools/tools/eval/python_eval_tool.rb +1 -1
  92. data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +1 -1
  93. data/lib/shared_tools/tools/eval/shell_eval_tool.rb +27 -6
  94. data/lib/shared_tools/tools/eval_tool.rb +1 -1
  95. data/lib/shared_tools/tools/gem_tool.rb +157 -0
  96. data/lib/shared_tools/tools/git/add_tool.rb +73 -0
  97. data/lib/shared_tools/tools/git/blame_tool.rb +77 -0
  98. data/lib/shared_tools/tools/git/branch_tool.rb +55 -0
  99. data/lib/shared_tools/tools/git/checkout_tool.rb +69 -0
  100. data/lib/shared_tools/tools/git/commit_tool.rb +73 -0
  101. data/lib/shared_tools/tools/git/diff_tool.rb +69 -0
  102. data/lib/shared_tools/tools/git/grep_tool.rb +82 -0
  103. data/lib/shared_tools/tools/git/helpers.rb +70 -0
  104. data/lib/shared_tools/tools/git/log_tool.rb +71 -0
  105. data/lib/shared_tools/tools/git/show_tool.rb +66 -0
  106. data/lib/shared_tools/tools/git/status_tool.rb +46 -0
  107. data/lib/shared_tools/tools/git.rb +18 -0
  108. data/lib/shared_tools/tools/git_tool.rb +148 -0
  109. data/lib/shared_tools/tools/glob_tool.rb +75 -0
  110. data/lib/shared_tools/tools/http_helpers.rb +119 -0
  111. data/lib/shared_tools/tools/http_request_tool.rb +91 -0
  112. data/lib/shared_tools/tools/json_query_tool.rb +81 -0
  113. data/lib/shared_tools/tools/lint_tool.rb +93 -0
  114. data/lib/shared_tools/tools/multi_edit_tool.rb +134 -0
  115. data/lib/shared_tools/tools/notification_tool.rb +1 -1
  116. data/lib/shared_tools/tools/parse_ruby_tool.rb +127 -0
  117. data/lib/shared_tools/tools/process_kill_tool.rb +55 -0
  118. data/lib/shared_tools/tools/process_list_tool.rb +41 -0
  119. data/lib/shared_tools/tools/process_output_tool.rb +69 -0
  120. data/lib/shared_tools/tools/process_start_tool.rb +0 -0
  121. data/lib/shared_tools/tools/python_tests_tool.rb +105 -0
  122. data/lib/shared_tools/tools/replace_in_files_tool.rb +166 -0
  123. data/lib/shared_tools/tools/run_tests_tool.rb +108 -0
  124. data/lib/shared_tools/tools/search_codebase_tool.rb +222 -0
  125. data/lib/shared_tools/tools/secure_tool_template.rb +1 -1
  126. data/lib/shared_tools/tools/system_info_tool.rb +1 -1
  127. data/lib/shared_tools/tools/todo_write_tool.rb +81 -0
  128. data/lib/shared_tools/tools/toml_query_tool.rb +81 -0
  129. data/lib/shared_tools/tools/toolchain_helpers.rb +64 -0
  130. data/lib/shared_tools/tools/tree_tool.rb +100 -0
  131. data/lib/shared_tools/tools/weather_tool.rb +12 -2
  132. data/lib/shared_tools/tools/web_fetch_tool.rb +85 -0
  133. data/lib/shared_tools/tools/workflow_manager_tool.rb +1 -1
  134. data/lib/shared_tools/tools/yaml_query_tool.rb +86 -0
  135. data/lib/shared_tools/tree_tool.rb +4 -0
  136. data/lib/shared_tools/url_guard.rb +106 -0
  137. data/lib/shared_tools/utilities.rb +2 -2
  138. data/lib/shared_tools/version.rb +1 -1
  139. data/lib/shared_tools/web_fetch_tool.rb +4 -0
  140. data/lib/shared_tools/yaml_query_tool.rb +4 -0
  141. data/lib/shared_tools.rb +1 -1
  142. metadata +516 -394
  143. data/lib/shared_tools/mcp/streamable_http_patch.rb +0 -42
  144. 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
@@ -15,7 +15,7 @@ module SharedTools
15
15
 
16
16
  description "A browser automation tool for clicking any clickable element."
17
17
 
18
- params do
18
+ parameters do
19
19
  string :selector, description: <<~TEXT.strip
20
20
  A CSS selector to locate or interact with an element on the page:
21
21
 
@@ -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
- params do
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
@@ -17,7 +17,7 @@ module SharedTools
17
17
 
18
18
  description "A browser automation tool for viewing the full HTML of the current page."
19
19
 
20
- params do
20
+ parameters do
21
21
  boolean :summarize, description: "If true, returns a summary instead of full HTML", required: false
22
22
  end
23
23
 
@@ -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
- params do
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
- params do
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
- params do
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
@@ -12,7 +12,7 @@ module SharedTools
12
12
 
13
13
  description "A browser automation tool for navigating to a specific URL."
14
14
 
15
- params do
15
+ parameters do
16
16
  string :url, description: "A URL (e.g. https://news.ycombinator.com)."
17
17
  end
18
18
 
@@ -86,7 +86,7 @@ module SharedTools
86
86
  {"action": "#{Action::SCREENSHOT}"}
87
87
  TEXT
88
88
 
89
- params do
89
+ parameters do
90
90
  string :action, description: <<~TEXT.strip
91
91
  The browser action to perform. Options:
92
92
  * `#{Action::VISIT}`: Navigate to a website
@@ -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
@@ -1,12 +1,22 @@
1
1
  # calculator_tool.rb - Safe mathematical calculator
2
2
  require 'ruby_llm/tool'
3
- require 'dentaku'
3
+
4
+ DENTAKU_AVAILABLE = begin
5
+ require 'dentaku'
6
+ true
7
+ rescue LoadError, Gem::LoadError
8
+ false
9
+ end
4
10
 
5
11
  module SharedTools
6
12
  module Tools
7
13
  class CalculatorTool < RubyLLM::Tool
8
14
  def self.name = 'calculator'
9
15
 
16
+ def available?
17
+ DENTAKU_AVAILABLE
18
+ end
19
+
10
20
  description <<~'DESCRIPTION'
11
21
  Perform advanced mathematical calculations with comprehensive error handling and validation.
12
22
  This tool supports basic arithmetic operations, parentheses, and common mathematical functions.
@@ -34,7 +44,7 @@ module SharedTools
34
44
  puts "Result: #{result[:result]}" # 3.3333
35
45
  DESCRIPTION
36
46
 
37
- params do
47
+ parameters do
38
48
  string :expression, description: <<~DESC.strip
39
49
  Mathematical expression to evaluate using standard arithmetic operators and parentheses.
40
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
- params do
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
- params do
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.
@@ -70,7 +70,7 @@ module SharedTools
70
70
  ScrollDirection::RIGHT,
71
71
  ].freeze
72
72
 
73
- params do
73
+ parameters do
74
74
  string :action, description: <<~TEXT.strip
75
75
  Options:
76
76
  * `#{Action::KEY}`: Press a single key / combination of keys on the keyboard:
@@ -33,7 +33,7 @@ module SharedTools
33
33
  'every weekday', 'first day of every month at midnight'
34
34
  DESC
35
35
 
36
- params do
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
@@ -24,7 +24,7 @@ module SharedTools
24
24
  - 'iso8601' — iso8601, iso8601_utc, unix_timestamp
25
25
  DESC
26
26
 
27
- params do
27
+ parameters do
28
28
  string :format, required: false, description: <<~DESC.strip
29
29
  Output format. Options: 'full' (default), 'date', 'time', 'iso8601'.
30
30
  DESC
@@ -17,7 +17,7 @@ module SharedTools
17
17
  recommendations and statistical significance testing where applicable.
18
18
  DESCRIPTION
19
19
 
20
- params do
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
@@ -1,12 +1,22 @@
1
1
  # database_query_tool.rb - Safe database query execution
2
2
  require 'ruby_llm/tool'
3
- require 'sequel'
3
+
4
+ SEQUEL_AVAILABLE = begin
5
+ require 'sequel'
6
+ true
7
+ rescue LoadError, Gem::LoadError
8
+ false
9
+ end
4
10
 
5
11
  module SharedTools
6
12
  module Tools
7
13
  class DatabaseQueryTool < RubyLLM::Tool
8
14
  def self.name = 'database_query'
9
15
 
16
+ def available?
17
+ SEQUEL_AVAILABLE
18
+ end
19
+
10
20
  description <<~'DESCRIPTION'
11
21
  Execute safe, read-only database queries with automatic connection management and security controls.
12
22
  This tool is designed for secure data retrieval operations only, restricting access to SELECT statements
@@ -31,7 +41,7 @@ module SharedTools
31
41
  puts "Found #{result[:row_count]} users"
32
42
  DESCRIPTION
33
43
 
34
- params do
44
+ parameters do
35
45
  string :query, description: <<~DESC.strip
36
46
  SQL SELECT query to execute against the database. Only SELECT statements are permitted
37
47
  for security reasons - INSERT, UPDATE, DELETE, and DDL statements will be rejected.
@@ -57,7 +57,7 @@ module SharedTools
57
57
  ]
58
58
  TEXT
59
59
 
60
- params do
60
+ parameters do
61
61
  array :statements, of: :string, description: "A list of SQL statements to run sequentially (e.g. ['SELECT * FROM users', 'INSERT INTO ...'])"
62
62
  end
63
63
 
@@ -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
@@ -13,7 +13,7 @@ module SharedTools
13
13
 
14
14
  description "Creates a directory."
15
15
 
16
- params do
16
+ parameters do
17
17
  string :path, description: "a path to the directory (e.g. `./foo/bar`)"
18
18
  end
19
19
 
@@ -13,7 +13,7 @@ module SharedTools
13
13
 
14
14
  description "Deletes a directory."
15
15
 
16
- params do
16
+ parameters do
17
17
  string :path, description: "a path to the directory (e.g. `./foo/bar`)"
18
18
  end
19
19
 
@@ -13,7 +13,7 @@ module SharedTools
13
13
 
14
14
  description "Summarizes the contents (files and directories) of a directory."
15
15
 
16
- params do
16
+ parameters do
17
17
  string :path, description: "a path to the directory (e.g. `./foo/bar`)", required: false
18
18
  end
19
19
 
@@ -13,7 +13,7 @@ module SharedTools
13
13
 
14
14
  description "Moves a directory from one location to another."
15
15
 
16
- params do
16
+ parameters do
17
17
  string :path, description: "a path (e.g. `./old`)"
18
18
  string :destination, description: "a path (e.g. `./new`)"
19
19
  end
@@ -13,7 +13,7 @@ module SharedTools
13
13
 
14
14
  description "Creates a file."
15
15
 
16
- params do
16
+ parameters do
17
17
  string :path, description: "a path to the file (e.g. `./README.md`)"
18
18
  end
19
19