tidewave 0.1.3 → 0.3.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/README.md +31 -32
- data/lib/tidewave/configuration.rb +7 -5
- data/lib/tidewave/database_adapter.rb +33 -0
- data/lib/tidewave/database_adapters/active_record.rb +39 -0
- data/lib/tidewave/database_adapters/sequel.rb +37 -0
- data/lib/tidewave/exceptions_middleware.rb +88 -0
- data/lib/tidewave/middleware.rb +184 -0
- data/lib/tidewave/quiet_requests_middleware.rb +15 -0
- data/lib/tidewave/railtie.rb +38 -32
- data/lib/tidewave/tools/execute_sql_query.rb +2 -20
- data/lib/tidewave/tools/get_docs.rb +64 -0
- data/lib/tidewave/tools/get_logs.rb +10 -3
- data/lib/tidewave/tools/get_models.rb +21 -10
- data/lib/tidewave/tools/get_source_location.rb +30 -10
- data/lib/tidewave/tools/project_eval.rb +49 -9
- data/lib/tidewave/version.rb +1 -1
- data/lib/tidewave.rb +6 -3
- metadata +10 -11
- data/lib/tidewave/file_tracker.rb +0 -101
- data/lib/tidewave/tools/edit_project_file.rb +0 -47
- data/lib/tidewave/tools/get_package_location.rb +0 -41
- data/lib/tidewave/tools/grep_project_files.rb +0 -110
- data/lib/tidewave/tools/list_project_files.rb +0 -26
- data/lib/tidewave/tools/package_search.rb +0 -43
- data/lib/tidewave/tools/read_project_file.rb +0 -25
- data/lib/tidewave/tools/shell_eval.rb +0 -36
- data/lib/tidewave/tools/write_project_file.rb +0 -28
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
|
4
|
+
tool_name "get_docs"
|
5
|
+
|
6
|
+
description <<~DESCRIPTION
|
7
|
+
Returns the documentation for the given reference.
|
8
|
+
|
9
|
+
The reference may be a constant, most commonly classes and modules
|
10
|
+
such as `String`, an instance method, such as `String#gsub`, or class
|
11
|
+
method, such as `File.executable?`
|
12
|
+
|
13
|
+
This works for methods in the current project, as well as dependencies.
|
14
|
+
|
15
|
+
This tool only works if you know the specific constant/method being targeted.
|
16
|
+
If that is the case, prefer this tool over grepping the file system.
|
17
|
+
DESCRIPTION
|
18
|
+
|
19
|
+
arguments do
|
20
|
+
required(:reference).filled(:string).description("The constant/method to lookup, such String, String#gsub or File.executable?")
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(reference:)
|
24
|
+
file_path, line_number = Tidewave::Tools::GetSourceLocation.get_source_location(reference)
|
25
|
+
|
26
|
+
if file_path
|
27
|
+
extract_documentation(file_path, line_number)
|
28
|
+
else
|
29
|
+
raise NameError, "could not find docs for #{reference}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def extract_documentation(file_path, line_number)
|
36
|
+
return nil unless File.exist?(file_path)
|
37
|
+
|
38
|
+
lines = File.readlines(file_path)
|
39
|
+
return nil if line_number <= 0 || line_number > lines.length
|
40
|
+
|
41
|
+
# Start from the line before the method definition
|
42
|
+
current_line = line_number - 2 # Convert to 0-based index and go one line up
|
43
|
+
comment_lines = []
|
44
|
+
|
45
|
+
# Collect comment lines going backwards
|
46
|
+
while current_line >= 0
|
47
|
+
line = lines[current_line].chomp.strip
|
48
|
+
|
49
|
+
if line.start_with?("#")
|
50
|
+
comment_lines.unshift(line.sub(/^#\s|^#/, ""))
|
51
|
+
elsif line.empty?
|
52
|
+
# Skip empty lines but continue looking for comments
|
53
|
+
else
|
54
|
+
# Hit a non-comment, non-empty line, stop collecting
|
55
|
+
break
|
56
|
+
end
|
57
|
+
|
58
|
+
current_line -= 1
|
59
|
+
end
|
60
|
+
|
61
|
+
return nil if comment_lines.empty?
|
62
|
+
comment_lines.join("\n")
|
63
|
+
end
|
64
|
+
end
|
@@ -10,13 +10,20 @@ class Tidewave::Tools::GetLogs < Tidewave::Tools::Base
|
|
10
10
|
|
11
11
|
arguments do
|
12
12
|
required(:tail).filled(:integer).description("The number of log entries to return from the end of the log")
|
13
|
+
optional(:grep).filled(:string).description("Filter logs with the given regular expression (case insensitive). E.g. \"error\" when you want to capture errors in particular")
|
13
14
|
end
|
14
15
|
|
15
|
-
def call(tail:)
|
16
|
+
def call(tail:, grep: nil)
|
16
17
|
log_file = Rails.root.join("log", "#{Rails.env}.log")
|
17
18
|
return "Log file not found" unless File.exist?(log_file)
|
18
19
|
|
19
|
-
logs = File.readlines(log_file)
|
20
|
-
|
20
|
+
logs = File.readlines(log_file)
|
21
|
+
|
22
|
+
if grep
|
23
|
+
regex = Regexp.new(grep, Regexp::IGNORECASE)
|
24
|
+
logs = logs.select { |line| line.match?(regex) }
|
25
|
+
end
|
26
|
+
|
27
|
+
logs.last(tail).join
|
21
28
|
end
|
22
29
|
end
|
@@ -3,25 +3,36 @@
|
|
3
3
|
class Tidewave::Tools::GetModels < Tidewave::Tools::Base
|
4
4
|
tool_name "get_models"
|
5
5
|
description <<~DESCRIPTION
|
6
|
-
Returns a list of all models in the application
|
6
|
+
Returns a list of all database-backed models in the application.
|
7
7
|
DESCRIPTION
|
8
8
|
|
9
9
|
def call
|
10
10
|
# Ensure all models are loaded
|
11
11
|
Rails.application.eager_load!
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
base_class = Tidewave::DatabaseAdapter.current.get_base_class
|
14
|
+
base_class.descendants.map do |model|
|
15
|
+
if location = get_relative_source_location(model.name)
|
16
|
+
"* #{model.name} at #{location}"
|
17
|
+
else
|
18
|
+
"* #{model.name}"
|
19
|
+
end
|
20
|
+
end.join("\n")
|
18
21
|
end
|
19
22
|
|
20
23
|
private
|
21
24
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
def get_relative_source_location(model_name)
|
26
|
+
source_location = Object.const_source_location(model_name)
|
27
|
+
return nil if source_location.blank?
|
28
|
+
|
29
|
+
file_path, line_number = source_location
|
30
|
+
begin
|
31
|
+
relative_path = Pathname.new(file_path).relative_path_from(Rails.root)
|
32
|
+
"#{relative_path}:#{line_number}"
|
33
|
+
rescue ArgumentError
|
34
|
+
# If the path cannot be made relative, return the absolute path
|
35
|
+
"#{file_path}:#{line_number}"
|
36
|
+
end
|
26
37
|
end
|
27
38
|
end
|
@@ -10,10 +10,11 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
|
|
10
10
|
such as `String`, an instance method, such as `String#gsub`, or class
|
11
11
|
method, such as `File.executable?`
|
12
12
|
|
13
|
-
This works
|
14
|
-
|
15
|
-
This tool only works if you know the specific constant/method being targeted.
|
13
|
+
This tool only works if you know the specific constant/method being targeted,
|
14
|
+
and it works across the current project and all dependencies.
|
16
15
|
If that is the case, prefer this tool over grepping the file system.
|
16
|
+
|
17
|
+
You may also get the root location of a gem, you can use "dep:PACKAGE_NAME".
|
17
18
|
DESCRIPTION
|
18
19
|
|
19
20
|
arguments do
|
@@ -21,21 +22,40 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def call(reference:)
|
24
|
-
|
25
|
+
# Check if this is a package location request
|
26
|
+
if reference.start_with?("dep:")
|
27
|
+
package_name = reference.gsub("dep:", "")
|
28
|
+
return get_package_location(package_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
file_path, line_number = self.class.get_source_location(reference)
|
25
32
|
|
26
33
|
if file_path
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
34
|
+
begin
|
35
|
+
relative_path = Pathname.new(file_path).relative_path_from(Rails.root)
|
36
|
+
"#{relative_path}:#{line_number}"
|
37
|
+
rescue ArgumentError
|
38
|
+
# If the path cannot be made relative, return the absolute path
|
39
|
+
"#{file_path}:#{line_number}"
|
40
|
+
end
|
31
41
|
else
|
32
42
|
raise NameError, "could not find source location for #{reference}"
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
36
|
-
|
46
|
+
def get_package_location(package)
|
47
|
+
raise "dep: prefix only works with projects using Bundler" unless defined?(Bundler)
|
48
|
+
specs = Bundler.load.specs
|
49
|
+
|
50
|
+
spec = specs.find { |s| s.name == package }
|
51
|
+
if spec
|
52
|
+
spec.full_gem_path
|
53
|
+
else
|
54
|
+
raise "Package #{package} not found. Check your Gemfile for available packages."
|
55
|
+
end
|
56
|
+
end
|
37
57
|
|
38
|
-
def get_source_location(reference)
|
58
|
+
def self.get_source_location(reference)
|
39
59
|
constant_path, selector, method_name = reference.rpartition(/\.|#/)
|
40
60
|
|
41
61
|
# There are no selectors, so the method_name is a constant path
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "timeout"
|
4
|
+
require "json"
|
5
|
+
|
3
6
|
class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
|
4
7
|
tool_name "project_eval"
|
5
8
|
description <<~DESCRIPTION
|
@@ -15,9 +18,12 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
|
|
15
18
|
|
16
19
|
arguments do
|
17
20
|
required(:code).filled(:string).description("The Ruby code to evaluate")
|
21
|
+
optional(:arguments).value(:array).description("The arguments to pass to evaluation. They are available inside the evaluated code as `arguments`.")
|
22
|
+
optional(:timeout).filled(:integer).description("The timeout in milliseconds. If the evaluation takes longer than this, it will be terminated. Defaults to 30000 (30 seconds).")
|
23
|
+
optional(:json).hidden().filled(:bool).description("Whether to return the result as JSON with structured output containing result, success, stdout, and stderr fields. Defaults to false.")
|
18
24
|
end
|
19
25
|
|
20
|
-
def call(code:)
|
26
|
+
def call(code:, arguments: [], timeout: 30_000, json: false)
|
21
27
|
original_stdout = $stdout
|
22
28
|
original_stderr = $stderr
|
23
29
|
|
@@ -27,22 +33,56 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
|
|
27
33
|
$stderr = stderr_capture
|
28
34
|
|
29
35
|
begin
|
30
|
-
|
36
|
+
timeout_seconds = timeout / 1000.0
|
37
|
+
|
38
|
+
success, result = begin
|
39
|
+
Timeout.timeout(timeout_seconds) do
|
40
|
+
[ true, eval(code, eval_binding(arguments)) ]
|
41
|
+
end
|
42
|
+
rescue Timeout::Error
|
43
|
+
[ false, "Timeout::Error: Evaluation timed out after #{timeout} milliseconds." ]
|
44
|
+
rescue => e
|
45
|
+
[ false, e.full_message ]
|
46
|
+
end
|
47
|
+
|
31
48
|
stdout = stdout_capture.string
|
32
49
|
stderr = stderr_capture.string
|
33
50
|
|
34
|
-
if
|
35
|
-
|
36
|
-
|
37
|
-
|
51
|
+
if json
|
52
|
+
JSON.generate({
|
53
|
+
result: result,
|
54
|
+
success: success,
|
38
55
|
stdout: stdout,
|
39
|
-
stderr: stderr
|
40
|
-
|
41
|
-
|
56
|
+
stderr: stderr
|
57
|
+
})
|
58
|
+
elsif stdout.empty? && stderr.empty?
|
59
|
+
# We explicitly call to_s so the result is not accidentally
|
60
|
+
# parsed as a JSON response by FastMCP.
|
61
|
+
result.to_s
|
62
|
+
else
|
63
|
+
<<~OUTPUT
|
64
|
+
STDOUT:
|
65
|
+
|
66
|
+
#{stdout}
|
67
|
+
|
68
|
+
STDERR:
|
69
|
+
|
70
|
+
#{stderr}
|
71
|
+
|
72
|
+
Result:
|
73
|
+
|
74
|
+
#{result}
|
75
|
+
OUTPUT
|
42
76
|
end
|
43
77
|
ensure
|
44
78
|
$stdout = original_stdout
|
45
79
|
$stderr = original_stderr
|
46
80
|
end
|
47
81
|
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def eval_binding(arguments)
|
86
|
+
binding
|
87
|
+
end
|
48
88
|
end
|
data/lib/tidewave/version.rb
CHANGED
data/lib/tidewave.rb
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
|
3
3
|
require "tidewave/version"
|
4
4
|
require "tidewave/railtie"
|
5
|
+
require "tidewave/database_adapter"
|
5
6
|
|
7
|
+
# Ensure DatabaseAdapters module is available
|
6
8
|
module Tidewave
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
module DatabaseAdapters
|
10
|
+
# This module is defined here to ensure it's available for autoloading
|
11
|
+
# Individual adapters are loaded on-demand in database_adapter.rb
|
12
|
+
end
|
10
13
|
end
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tidewave
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yorick Jacquin
|
8
|
+
- José Valim
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2025-
|
12
|
+
date: 2025-09-08 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rails
|
@@ -64,22 +65,20 @@ files:
|
|
64
65
|
- config/database.yml
|
65
66
|
- lib/tidewave.rb
|
66
67
|
- lib/tidewave/configuration.rb
|
67
|
-
- lib/tidewave/
|
68
|
+
- lib/tidewave/database_adapter.rb
|
69
|
+
- lib/tidewave/database_adapters/active_record.rb
|
70
|
+
- lib/tidewave/database_adapters/sequel.rb
|
71
|
+
- lib/tidewave/exceptions_middleware.rb
|
72
|
+
- lib/tidewave/middleware.rb
|
73
|
+
- lib/tidewave/quiet_requests_middleware.rb
|
68
74
|
- lib/tidewave/railtie.rb
|
69
75
|
- lib/tidewave/tools/base.rb
|
70
|
-
- lib/tidewave/tools/edit_project_file.rb
|
71
76
|
- lib/tidewave/tools/execute_sql_query.rb
|
77
|
+
- lib/tidewave/tools/get_docs.rb
|
72
78
|
- lib/tidewave/tools/get_logs.rb
|
73
79
|
- lib/tidewave/tools/get_models.rb
|
74
|
-
- lib/tidewave/tools/get_package_location.rb
|
75
80
|
- lib/tidewave/tools/get_source_location.rb
|
76
|
-
- lib/tidewave/tools/grep_project_files.rb
|
77
|
-
- lib/tidewave/tools/list_project_files.rb
|
78
|
-
- lib/tidewave/tools/package_search.rb
|
79
81
|
- lib/tidewave/tools/project_eval.rb
|
80
|
-
- lib/tidewave/tools/read_project_file.rb
|
81
|
-
- lib/tidewave/tools/shell_eval.rb
|
82
|
-
- lib/tidewave/tools/write_project_file.rb
|
83
82
|
- lib/tidewave/version.rb
|
84
83
|
homepage: https://tidewave.ai/
|
85
84
|
licenses:
|
@@ -1,101 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Tidewave
|
4
|
-
module FileTracker
|
5
|
-
extend self
|
6
|
-
|
7
|
-
def project_files(glob_pattern: nil)
|
8
|
-
args = %w[--git-dir] + [ "#{git_root}/.git", "ls-files", "--cached", "--others" ]
|
9
|
-
args += glob_pattern ? [ glob_pattern ] : [ "--exclude-standard" ]
|
10
|
-
`git #{args.join(" ")}`.split("\n")
|
11
|
-
end
|
12
|
-
|
13
|
-
def read_file(path, line_offset: 0, count: nil)
|
14
|
-
full_path = file_full_path(path)
|
15
|
-
# Explicitly read the mtime first to avoid race conditions
|
16
|
-
mtime = File.mtime(full_path).to_i
|
17
|
-
content = File.read(full_path)
|
18
|
-
|
19
|
-
if line_offset > 0 || count
|
20
|
-
lines = content.lines
|
21
|
-
start_idx = [ line_offset, 0 ].max
|
22
|
-
count = (count || lines.length)
|
23
|
-
selected_lines = lines[start_idx, count]
|
24
|
-
content = selected_lines ? selected_lines.join : ""
|
25
|
-
end
|
26
|
-
|
27
|
-
[ mtime, content ]
|
28
|
-
end
|
29
|
-
|
30
|
-
def write_file(path, content)
|
31
|
-
validate_ruby_syntax!(content) if ruby_file?(path)
|
32
|
-
full_path = file_full_path(path)
|
33
|
-
|
34
|
-
# Create the directory if it doesn't exist
|
35
|
-
dirname = File.dirname(full_path)
|
36
|
-
FileUtils.mkdir_p(dirname)
|
37
|
-
|
38
|
-
# Write and return the file contents
|
39
|
-
File.write(full_path, content)
|
40
|
-
content
|
41
|
-
end
|
42
|
-
|
43
|
-
def file_full_path(path)
|
44
|
-
File.join(git_root, path)
|
45
|
-
end
|
46
|
-
|
47
|
-
def git_root
|
48
|
-
@git_root ||= `git rev-parse --show-toplevel`.strip
|
49
|
-
end
|
50
|
-
|
51
|
-
def validate_path_access!(path, validate_existence: true)
|
52
|
-
raise ArgumentError, "File path must not contain '..'" if path.include?("..")
|
53
|
-
|
54
|
-
# Ensure the path is within the project
|
55
|
-
full_path = file_full_path(path)
|
56
|
-
|
57
|
-
# Verify the file is within the project directory
|
58
|
-
raise ArgumentError, "File path must be within the project directory" unless full_path.start_with?(git_root)
|
59
|
-
|
60
|
-
# Verify the file exists
|
61
|
-
raise ArgumentError, "File not found: #{path}" if validate_existence && !File.exist?(full_path)
|
62
|
-
|
63
|
-
true
|
64
|
-
end
|
65
|
-
|
66
|
-
def validate_path_is_editable!(path, atime)
|
67
|
-
validate_path_access!(path)
|
68
|
-
validate_path_has_been_read_since_last_write!(path, atime)
|
69
|
-
|
70
|
-
true
|
71
|
-
end
|
72
|
-
|
73
|
-
def validate_path_is_writable!(path, atime)
|
74
|
-
validate_path_access!(path, validate_existence: false)
|
75
|
-
validate_path_has_been_read_since_last_write!(path, atime)
|
76
|
-
|
77
|
-
true
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def ruby_file?(path)
|
83
|
-
[ ".rb", ".rake", ".gemspec" ].include?(File.extname(path)) ||
|
84
|
-
[ "Gemfile" ].include?(File.basename(path))
|
85
|
-
end
|
86
|
-
|
87
|
-
def validate_ruby_syntax!(content)
|
88
|
-
RubyVM::AbstractSyntaxTree.parse(content)
|
89
|
-
rescue SyntaxError => e
|
90
|
-
raise "Invalid Ruby syntax: #{e.message}"
|
91
|
-
end
|
92
|
-
|
93
|
-
def validate_path_has_been_read_since_last_write!(path, atime)
|
94
|
-
if atime && File.mtime(file_full_path(path)).to_i > atime
|
95
|
-
raise ArgumentError, "File has been modified since last read, please read the file again"
|
96
|
-
end
|
97
|
-
|
98
|
-
true
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "tidewave/file_tracker"
|
4
|
-
|
5
|
-
class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
|
6
|
-
tags :file_system_tool
|
7
|
-
|
8
|
-
tool_name "edit_project_file"
|
9
|
-
description <<~DESCRIPTION
|
10
|
-
A tool for editing parts of a file. It can find and replace text inside a file.
|
11
|
-
For moving or deleting files, use the shell_eval tool with 'mv' or 'rm' instead.
|
12
|
-
|
13
|
-
For large edits, use the write_project_file tool instead and overwrite the entire file.
|
14
|
-
|
15
|
-
Before editing, ensure to read the source file using the read_project_file tool.
|
16
|
-
|
17
|
-
To use this tool, provide the path to the file, the old_string to search for, and the new_string to replace it with.
|
18
|
-
If the old_string is found multiple times, an error will be returned. To ensure uniqueness, include a couple of lines
|
19
|
-
before and after the edit. All whitespace must be preserved as in the original file.
|
20
|
-
|
21
|
-
This tool can only do a single edit at a time. If you need to make multiple edits, you can create a message with
|
22
|
-
multiple tool calls to this tool, ensuring that each one contains enough context to uniquely identify the edit.
|
23
|
-
DESCRIPTION
|
24
|
-
|
25
|
-
arguments do
|
26
|
-
required(:path).filled(:string).description("The path to the file to edit. It is relative to the project root.")
|
27
|
-
required(:old_string).filled(:string).description("The string to search for")
|
28
|
-
required(:new_string).filled(:string).description("The string to replace the old_string with")
|
29
|
-
optional(:atime).filled(:integer).hidden.description("The Unix timestamp this file was last accessed. Not to be used.")
|
30
|
-
end
|
31
|
-
|
32
|
-
def call(path:, old_string:, new_string:, atime: nil)
|
33
|
-
# Check if the file exists within the project root and has been read
|
34
|
-
Tidewave::FileTracker.validate_path_is_editable!(path, atime)
|
35
|
-
|
36
|
-
old_content = Tidewave::FileTracker.read_file(path)
|
37
|
-
|
38
|
-
# Ensure old_string is unique within the file
|
39
|
-
scan_result = old_content.scan(old_string)
|
40
|
-
raise ArgumentError, "old_string is not found" if scan_result.empty?
|
41
|
-
raise ArgumentError, "old_string is not unique" if scan_result.size > 1
|
42
|
-
|
43
|
-
new_content = old_content.sub(old_string, new_string)
|
44
|
-
Tidewave::FileTracker.write_file(path, new_content)
|
45
|
-
"OK"
|
46
|
-
end
|
47
|
-
end
|
@@ -1,41 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pathname"
|
4
|
-
|
5
|
-
class Tidewave::Tools::GetPackageLocation < Tidewave::Tools::Base
|
6
|
-
tool_name "get_package_location"
|
7
|
-
|
8
|
-
description <<~DESCRIPTION
|
9
|
-
Returns the location of dependency packages.
|
10
|
-
You can use this tool to get the location of any project dependency. Optionally,
|
11
|
-
a specific dependency name can be provided to only return the location of that dependency.
|
12
|
-
Use the result in combination with shell tools like grep to look for specific
|
13
|
-
code inside dependencies.
|
14
|
-
DESCRIPTION
|
15
|
-
|
16
|
-
arguments do
|
17
|
-
optional(:package).maybe(:string).description(
|
18
|
-
"The name of the package to get the location of. If not provided, the location of all packages will be returned."
|
19
|
-
)
|
20
|
-
end
|
21
|
-
|
22
|
-
def call(package: nil)
|
23
|
-
raise "get_package_location only works with projects using Bundler" unless defined?(Bundler)
|
24
|
-
specs = Bundler.load.specs
|
25
|
-
|
26
|
-
if package
|
27
|
-
spec = specs.find { |s| s.name == package }
|
28
|
-
if spec
|
29
|
-
spec.full_gem_path
|
30
|
-
else
|
31
|
-
raise "Package #{package} not found. Check your Gemfile for available packages."
|
32
|
-
end
|
33
|
-
else
|
34
|
-
# For all packages, return a formatted string with package names and locations
|
35
|
-
specs.map do |spec|
|
36
|
-
relative_path = Pathname.new(spec.full_gem_path).relative_path_from(Pathname.new(Dir.pwd))
|
37
|
-
"#{spec.name}: #{relative_path}"
|
38
|
-
end.join("\n")
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Tidewave::Tools::GrepProjectFiles < Tidewave::Tools::Base
|
4
|
-
tags :file_system_tool
|
5
|
-
|
6
|
-
def self.ripgrep_executable
|
7
|
-
@ripgrep_executable ||= `which rg`.strip
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.ripgrep_available?
|
11
|
-
ripgrep_executable.present?
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.description
|
15
|
-
"Searches for text patterns in files using #{ripgrep_available? ? 'ripgrep' : 'a grep variant'}."
|
16
|
-
end
|
17
|
-
tool_name "grep_project_files"
|
18
|
-
|
19
|
-
arguments do
|
20
|
-
required(:pattern).filled(:string).description("The pattern to search for")
|
21
|
-
optional(:glob).filled(:string).description(
|
22
|
-
'Optional glob pattern to filter which files to search in, e.g., \"**/*.ex\". Note that if a glob pattern is used, the .gitignore file will be ignored.'
|
23
|
-
)
|
24
|
-
optional(:case_sensitive).filled(:bool).description("Whether the search should be case-sensitive. Defaults to false.")
|
25
|
-
optional(:max_results).filled(:integer).description("Maximum number of results to return. Defaults to 100.")
|
26
|
-
end
|
27
|
-
|
28
|
-
def call(pattern:, glob: "**/*", case_sensitive: false, max_results: 100)
|
29
|
-
if self.class.ripgrep_available?
|
30
|
-
execute_ripgrep(pattern, glob, case_sensitive, max_results)
|
31
|
-
else
|
32
|
-
execute_grep(pattern, glob, case_sensitive, max_results)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def execute_ripgrep(pattern, glob, case_sensitive, max_results)
|
39
|
-
command = [ self.class.ripgrep_executable ]
|
40
|
-
command << "--no-require-git" # ignore gitignored files
|
41
|
-
command << "--json" # formatted as json
|
42
|
-
command << "--max-count=#{max_results}"
|
43
|
-
command << "--ignore-case" unless case_sensitive
|
44
|
-
command << "--glob=#{glob}" if glob.present?
|
45
|
-
command << pattern
|
46
|
-
command << "." # Search in current directory
|
47
|
-
|
48
|
-
results = `#{command.join(" ")} 2>&1`
|
49
|
-
|
50
|
-
# Process the results as needed
|
51
|
-
format_ripgrep_results(results)
|
52
|
-
end
|
53
|
-
|
54
|
-
def execute_grep(pattern, glob, case_sensitive, max_results)
|
55
|
-
glob = "**/*" if glob.blank?
|
56
|
-
files = Dir.glob(glob, base: Tidewave::FileTracker.git_root)
|
57
|
-
results = []
|
58
|
-
files.each do |file|
|
59
|
-
full_path = File.join(Tidewave::FileTracker.git_root, file)
|
60
|
-
next unless File.file?(full_path)
|
61
|
-
|
62
|
-
begin
|
63
|
-
file_matches = 0
|
64
|
-
line_number = 0
|
65
|
-
|
66
|
-
File.foreach(full_path) do |line|
|
67
|
-
line_number += 1
|
68
|
-
|
69
|
-
# Check if line matches pattern with proper case sensitivity
|
70
|
-
if case_sensitive
|
71
|
-
next unless line.include?(pattern)
|
72
|
-
else
|
73
|
-
next unless line.downcase.include?(pattern.downcase)
|
74
|
-
end
|
75
|
-
|
76
|
-
results << {
|
77
|
-
"path" => file,
|
78
|
-
"line_number" => line_number,
|
79
|
-
"content" => line.strip
|
80
|
-
}
|
81
|
-
|
82
|
-
file_matches += 1
|
83
|
-
# Stop processing this file if we've reached max results for it
|
84
|
-
break if file_matches >= max_results
|
85
|
-
end
|
86
|
-
rescue => e
|
87
|
-
# Skip files that can't be read (e.g., binary files)
|
88
|
-
next
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
results.to_json
|
93
|
-
end
|
94
|
-
|
95
|
-
def format_ripgrep_results(results)
|
96
|
-
parsed_results = results.split("\n").map(&:strip).reject(&:empty?).map do |line|
|
97
|
-
JSON.parse(line)
|
98
|
-
end
|
99
|
-
|
100
|
-
parsed_results.map do |result|
|
101
|
-
next if result["type"] != "match"
|
102
|
-
|
103
|
-
{
|
104
|
-
"path" => result.dig("data", "path", "text"),
|
105
|
-
"line_number" => result.dig("data", "line_number"),
|
106
|
-
"content" => result.dig("data", "lines", "text").strip
|
107
|
-
}
|
108
|
-
end.compact.to_json
|
109
|
-
end
|
110
|
-
end
|