tidewave 0.1.2 → 0.1.3
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/lib/tidewave/file_tracker.rb +39 -61
- data/lib/tidewave/railtie.rb +13 -4
- data/lib/tidewave/tools/base.rb +0 -7
- data/lib/tidewave/tools/edit_project_file.rb +5 -4
- data/lib/tidewave/tools/get_package_location.rb +41 -0
- data/lib/tidewave/tools/get_source_location.rb +31 -29
- data/lib/tidewave/tools/grep_project_files.rb +6 -4
- data/lib/tidewave/tools/list_project_files.rb +16 -4
- data/lib/tidewave/tools/package_search.rb +8 -1
- data/lib/tidewave/tools/read_project_file.rb +9 -4
- data/lib/tidewave/tools/shell_eval.rb +1 -1
- data/lib/tidewave/tools/write_project_file.rb +7 -4
- data/lib/tidewave/version.rb +1 -1
- metadata +5 -6
- data/lib/tidewave/tool_resolver.rb +0 -72
- data/lib/tidewave/tools/glob_project_files.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d4f421f00436367f21d189e0fe49ea8a1ce4f611457b720f62027b4b8f080dc
|
4
|
+
data.tar.gz: 6bb95b976f34b5520b54fb8a0789d1adf18c591cef285bcd5500f591b2c4d1a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 851460abeef4e3d29d0d475a83f1e962bb0a6279708e25cc25ed2b8620782e48f837f8db279fac1da822af3c1d5472208be2340ca691ef5a226ab7bd7f4292f9
|
7
|
+
data.tar.gz: 48f5e731497bf822329c2de6865361c0d31bfe5fa64afed0c213d75d8de4db08ed4f44aa7ef76e889803330c70ba202712f724d569650d2707dccd3debb74d11
|
@@ -4,38 +4,40 @@ module Tidewave
|
|
4
4
|
module FileTracker
|
5
5
|
extend self
|
6
6
|
|
7
|
-
def project_files
|
8
|
-
|
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")
|
9
11
|
end
|
10
12
|
|
11
|
-
def read_file(path)
|
12
|
-
validate_path_access!(path)
|
13
|
-
|
14
|
-
# Retrieve the full path
|
13
|
+
def read_file(path, line_offset: 0, count: nil)
|
15
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)
|
16
18
|
|
17
|
-
|
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
|
19
26
|
|
20
|
-
|
21
|
-
File.read(full_path)
|
27
|
+
[ mtime, content ]
|
22
28
|
end
|
23
29
|
|
24
30
|
def write_file(path, content)
|
25
|
-
|
26
|
-
# Retrieve the full path
|
31
|
+
validate_ruby_syntax!(content) if ruby_file?(path)
|
27
32
|
full_path = file_full_path(path)
|
28
33
|
|
29
|
-
dirname = File.dirname(full_path)
|
30
|
-
|
31
34
|
# Create the directory if it doesn't exist
|
35
|
+
dirname = File.dirname(full_path)
|
32
36
|
FileUtils.mkdir_p(dirname)
|
33
37
|
|
34
|
-
# Write the file contents
|
38
|
+
# Write and return the file contents
|
35
39
|
File.write(full_path, content)
|
36
|
-
|
37
|
-
# Read and return the file contents
|
38
|
-
read_file(path)
|
40
|
+
content
|
39
41
|
end
|
40
42
|
|
41
43
|
def file_full_path(path)
|
@@ -47,7 +49,7 @@ module Tidewave
|
|
47
49
|
end
|
48
50
|
|
49
51
|
def validate_path_access!(path, validate_existence: true)
|
50
|
-
raise ArgumentError, "File path must not
|
52
|
+
raise ArgumentError, "File path must not contain '..'" if path.include?("..")
|
51
53
|
|
52
54
|
# Ensure the path is within the project
|
53
55
|
full_path = file_full_path(path)
|
@@ -56,68 +58,44 @@ module Tidewave
|
|
56
58
|
raise ArgumentError, "File path must be within the project directory" unless full_path.start_with?(git_root)
|
57
59
|
|
58
60
|
# Verify the file exists
|
59
|
-
raise ArgumentError, "File not found: #{path}"
|
61
|
+
raise ArgumentError, "File not found: #{path}" if validate_existence && !File.exist?(full_path)
|
60
62
|
|
61
63
|
true
|
62
64
|
end
|
63
65
|
|
64
|
-
def validate_path_is_editable!(path)
|
66
|
+
def validate_path_is_editable!(path, atime)
|
65
67
|
validate_path_access!(path)
|
66
|
-
validate_path_has_been_read_since_last_write!(path)
|
68
|
+
validate_path_has_been_read_since_last_write!(path, atime)
|
67
69
|
|
68
70
|
true
|
69
71
|
end
|
70
72
|
|
71
|
-
def validate_path_is_writable!(path)
|
73
|
+
def validate_path_is_writable!(path, atime)
|
72
74
|
validate_path_access!(path, validate_existence: false)
|
73
|
-
validate_path_has_been_read_since_last_write!(path)
|
75
|
+
validate_path_has_been_read_since_last_write!(path, atime)
|
74
76
|
|
75
77
|
true
|
76
78
|
end
|
77
79
|
|
78
|
-
|
79
|
-
raise ArgumentError, "File has been modified since last read, please read the file again" unless file_was_read_since_last_write?(path)
|
80
|
+
private
|
80
81
|
|
81
|
-
|
82
|
+
def ruby_file?(path)
|
83
|
+
[ ".rb", ".rake", ".gemspec" ].include?(File.extname(path)) ||
|
84
|
+
[ "Gemfile" ].include?(File.basename(path))
|
82
85
|
end
|
83
86
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
+
def validate_ruby_syntax!(content)
|
88
|
+
RubyVM::AbstractSyntaxTree.parse(content)
|
89
|
+
rescue SyntaxError => e
|
90
|
+
raise "Invalid Ruby syntax: #{e.message}"
|
87
91
|
end
|
88
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
|
89
97
|
|
90
|
-
|
91
|
-
file_was_read?(path) && last_read_at(path) >= last_modified_at(path)
|
92
|
-
end
|
93
|
-
|
94
|
-
# Check if a file has been read
|
95
|
-
def file_was_read?(path)
|
96
|
-
file_records.key?(path)
|
97
|
-
end
|
98
|
-
|
99
|
-
# Check if a file exists
|
100
|
-
def file_exists?(path)
|
101
|
-
File.exist?(file_full_path(path))
|
102
|
-
end
|
103
|
-
|
104
|
-
# Get the timestamp when a file was last read
|
105
|
-
def last_read_at(path)
|
106
|
-
file_records[path]
|
107
|
-
end
|
108
|
-
|
109
|
-
def last_modified_at(path)
|
110
|
-
File.mtime(file_full_path(path))
|
111
|
-
end
|
112
|
-
|
113
|
-
# Reset all tracked files (useful for testing)
|
114
|
-
def reset
|
115
|
-
@file_records = {}
|
116
|
-
end
|
117
|
-
|
118
|
-
# Hash mapping file paths to their read records
|
119
|
-
def file_records
|
120
|
-
@file_records ||= {}
|
98
|
+
true
|
121
99
|
end
|
122
100
|
end
|
123
101
|
end
|
data/lib/tidewave/railtie.rb
CHANGED
@@ -3,8 +3,11 @@
|
|
3
3
|
require "fast_mcp"
|
4
4
|
require "logger"
|
5
5
|
require "fileutils"
|
6
|
-
require "tidewave/tool_resolver"
|
7
6
|
require "tidewave/configuration"
|
7
|
+
require "active_support/core_ext/class"
|
8
|
+
|
9
|
+
gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
|
10
|
+
Dir[gem_tools_path].each { |f| require f }
|
8
11
|
|
9
12
|
module Tidewave
|
10
13
|
class Railtie < Rails::Railtie
|
@@ -30,9 +33,15 @@ module Tidewave
|
|
30
33
|
allowed_ips: config.allowed_ips
|
31
34
|
) do |server|
|
32
35
|
app.config.before_initialize do
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
server.filter_tools do |request, tools|
|
37
|
+
if request.params["include_fs_tools"] != "true"
|
38
|
+
tools.reject { |tool| tool.tags.include?(:file_system_tool) }
|
39
|
+
else
|
40
|
+
tools
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
server.register_tools(*Tidewave::Tools::Base.descendants)
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
data/lib/tidewave/tools/base.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "tidewave/file_tracker"
|
4
4
|
|
5
5
|
class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
|
6
|
-
file_system_tool
|
6
|
+
tags :file_system_tool
|
7
7
|
|
8
8
|
tool_name "edit_project_file"
|
9
9
|
description <<~DESCRIPTION
|
@@ -26,11 +26,12 @@ class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
|
|
26
26
|
required(:path).filled(:string).description("The path to the file to edit. It is relative to the project root.")
|
27
27
|
required(:old_string).filled(:string).description("The string to search for")
|
28
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.")
|
29
30
|
end
|
30
31
|
|
31
|
-
def call(path:, old_string:, new_string:)
|
32
|
+
def call(path:, old_string:, new_string:, atime: nil)
|
32
33
|
# Check if the file exists within the project root and has been read
|
33
|
-
Tidewave::FileTracker.validate_path_is_editable!(path)
|
34
|
+
Tidewave::FileTracker.validate_path_is_editable!(path, atime)
|
34
35
|
|
35
36
|
old_content = Tidewave::FileTracker.read_file(path)
|
36
37
|
|
@@ -40,7 +41,7 @@ class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
|
|
40
41
|
raise ArgumentError, "old_string is not unique" if scan_result.size > 1
|
41
42
|
|
42
43
|
new_content = old_content.sub(old_string, new_string)
|
43
|
-
|
44
44
|
Tidewave::FileTracker.write_file(path, new_content)
|
45
|
+
"OK"
|
45
46
|
end
|
46
47
|
end
|
@@ -0,0 +1,41 @@
|
|
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,58 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/core_ext/string/inflections"
|
4
|
-
require "active_support/core_ext/object/blank"
|
5
|
-
|
6
3
|
class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
|
7
4
|
tool_name "get_source_location"
|
8
5
|
|
9
6
|
description <<~DESCRIPTION
|
10
|
-
Returns the source location for the given
|
7
|
+
Returns the source location 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?`
|
11
12
|
|
12
|
-
This works for
|
13
|
+
This works for methods in the current project, as well as dependencies.
|
13
14
|
|
14
|
-
This tool only works if you know the specific
|
15
|
+
This tool only works if you know the specific constant/method being targeted.
|
15
16
|
If that is the case, prefer this tool over grepping the file system.
|
16
17
|
DESCRIPTION
|
17
18
|
|
18
19
|
arguments do
|
19
|
-
required(:
|
20
|
-
optional(:function_name).filled(:string).description("The function to get source location for. When used, a module must also be passed.")
|
20
|
+
required(:reference).filled(:string).description("The constant/method to lookup, such String, String#gsub or File.executable?")
|
21
21
|
end
|
22
22
|
|
23
|
+
def call(reference:)
|
24
|
+
file_path, line_number = get_source_location(reference)
|
23
25
|
|
24
|
-
|
25
|
-
file_path, line_number = get_source_location(module_name, function_name)
|
26
|
-
|
26
|
+
if file_path
|
27
27
|
{
|
28
28
|
file_path: file_path,
|
29
29
|
line_number: line_number
|
30
30
|
}.to_json
|
31
|
+
else
|
32
|
+
raise NameError, "could not find source location for #{reference}"
|
33
|
+
end
|
31
34
|
end
|
32
35
|
|
33
36
|
private
|
34
37
|
|
35
|
-
def get_source_location(
|
36
|
-
|
37
|
-
module_ref = module_name.constantize
|
38
|
-
rescue NameError
|
39
|
-
raise NameError, "Module #{module_name} not found"
|
40
|
-
end
|
38
|
+
def get_source_location(reference)
|
39
|
+
constant_path, selector, method_name = reference.rpartition(/\.|#/)
|
41
40
|
|
42
|
-
|
41
|
+
# There are no selectors, so the method_name is a constant path
|
42
|
+
return Object.const_source_location(method_name) if selector.empty?
|
43
43
|
|
44
|
-
|
45
|
-
|
44
|
+
begin
|
45
|
+
mod = Object.const_get(constant_path)
|
46
|
+
rescue NameError => e
|
47
|
+
raise e
|
48
|
+
rescue
|
49
|
+
raise "wrong or invalid reference #{reference}"
|
50
|
+
end
|
46
51
|
|
47
|
-
|
48
|
-
module_ref.method(function_name).source_location
|
49
|
-
rescue NameError
|
50
|
-
get_instance_method_definition(module_ref, function_name)
|
51
|
-
end
|
52
|
+
raise "reference #{constant_path} does not point a class/module" unless mod.is_a?(Module)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
if selector == "#"
|
55
|
+
mod.instance_method(method_name).source_location
|
56
|
+
else
|
57
|
+
mod.method(method_name).source_location
|
58
|
+
end
|
57
59
|
end
|
58
60
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Tidewave::Tools::GrepProjectFiles < Tidewave::Tools::Base
|
4
|
-
file_system_tool
|
4
|
+
tags :file_system_tool
|
5
5
|
|
6
6
|
def self.ripgrep_executable
|
7
7
|
@ripgrep_executable ||= `which rg`.strip
|
@@ -52,16 +52,18 @@ class Tidewave::Tools::GrepProjectFiles < Tidewave::Tools::Base
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def execute_grep(pattern, glob, case_sensitive, max_results)
|
55
|
-
|
55
|
+
glob = "**/*" if glob.blank?
|
56
|
+
files = Dir.glob(glob, base: Tidewave::FileTracker.git_root)
|
56
57
|
results = []
|
57
58
|
files.each do |file|
|
58
|
-
|
59
|
+
full_path = File.join(Tidewave::FileTracker.git_root, file)
|
60
|
+
next unless File.file?(full_path)
|
59
61
|
|
60
62
|
begin
|
61
63
|
file_matches = 0
|
62
64
|
line_number = 0
|
63
65
|
|
64
|
-
File.foreach(
|
66
|
+
File.foreach(full_path) do |line|
|
65
67
|
line_number += 1
|
66
68
|
|
67
69
|
# Check if line matches pattern with proper case sensitivity
|
@@ -3,12 +3,24 @@
|
|
3
3
|
require "tidewave/file_tracker"
|
4
4
|
|
5
5
|
class Tidewave::Tools::ListProjectFiles < Tidewave::Tools::Base
|
6
|
-
file_system_tool
|
6
|
+
tags :file_system_tool
|
7
7
|
|
8
8
|
tool_name "list_project_files"
|
9
|
-
description
|
9
|
+
description <<~DESC
|
10
|
+
Returns a list of files in the project.
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
By default, when no arguments are passed, it returns all files in the project that
|
13
|
+
are not ignored by .gitignore.
|
14
|
+
|
15
|
+
Optionally, a glob_pattern can be passed to filter this list. When a pattern is passed,
|
16
|
+
the gitignore check will be skipped.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
arguments do
|
20
|
+
optional(:glob_pattern).maybe(:string).description("Optional: a glob pattern to filter the listed files. If a pattern is passed, the .gitignore check will be skipped.")
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(glob_pattern: nil)
|
24
|
+
Tidewave::FileTracker.project_files(glob_pattern: glob_pattern)
|
13
25
|
end
|
14
26
|
end
|
@@ -28,7 +28,14 @@ class Tidewave::Tools::PackageSearch < Tidewave::Tools::Base
|
|
28
28
|
response = Net::HTTP.get_response(uri)
|
29
29
|
|
30
30
|
if response.is_a?(Net::HTTPSuccess)
|
31
|
-
JSON.parse(response.body)
|
31
|
+
JSON.parse(response.body).map do |package|
|
32
|
+
{
|
33
|
+
name: package["name"],
|
34
|
+
version: package["version"],
|
35
|
+
downloads: package["downloads"],
|
36
|
+
documentation_uri: package["documentation_uri"]
|
37
|
+
}
|
38
|
+
end
|
32
39
|
else
|
33
40
|
raise "RubyGems API request failed with status code: #{response.code}"
|
34
41
|
end
|
@@ -3,18 +3,23 @@
|
|
3
3
|
require "tidewave/file_tracker"
|
4
4
|
|
5
5
|
class Tidewave::Tools::ReadProjectFile < Tidewave::Tools::Base
|
6
|
-
file_system_tool
|
6
|
+
tags :file_system_tool
|
7
7
|
|
8
8
|
tool_name "read_project_file"
|
9
9
|
description <<~DESCRIPTION
|
10
|
-
Returns the contents of the given file.
|
10
|
+
Returns the contents of the given file.
|
11
|
+
Supports an optional line_offset and count. To read the full file, only the path needs to be passed.
|
11
12
|
DESCRIPTION
|
12
13
|
|
13
14
|
arguments do
|
14
15
|
required(:path).filled(:string).description("The path to the file to read. It is relative to the project root.")
|
16
|
+
optional(:line_offset).filled(:integer).description("Optional: the starting line offset from which to read. Defaults to 0.")
|
17
|
+
optional(:count).filled(:integer).description("Optional: the number of lines to read. Defaults to all.")
|
15
18
|
end
|
16
19
|
|
17
|
-
def call(path
|
18
|
-
Tidewave::FileTracker.
|
20
|
+
def call(path:, **keywords)
|
21
|
+
Tidewave::FileTracker.validate_path_access!(path)
|
22
|
+
_meta[:mtime], contents = Tidewave::FileTracker.read_file(path, **keywords)
|
23
|
+
contents
|
19
24
|
end
|
20
25
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
require "tidewave/file_tracker"
|
4
4
|
|
5
5
|
class Tidewave::Tools::WriteProjectFile < Tidewave::Tools::Base
|
6
|
-
file_system_tool
|
6
|
+
tags :file_system_tool
|
7
7
|
|
8
8
|
tool_name "write_project_file"
|
9
9
|
description <<~DESCRIPTION
|
@@ -15,11 +15,14 @@ class Tidewave::Tools::WriteProjectFile < Tidewave::Tools::Base
|
|
15
15
|
arguments do
|
16
16
|
required(:path).filled(:string).description("The path to the file to write. It is relative to the project root.")
|
17
17
|
required(:content).filled(:string).description("The content to write to the file")
|
18
|
+
optional(:atime).filled(:integer).hidden.description("The Unix timestamp this file was last accessed. Not to be used.")
|
18
19
|
end
|
19
20
|
|
20
|
-
def call(path:, content:)
|
21
|
-
Tidewave::FileTracker.validate_path_is_writable!(path)
|
22
|
-
|
21
|
+
def call(path:, content:, atime: nil)
|
22
|
+
Tidewave::FileTracker.validate_path_is_writable!(path, atime)
|
23
23
|
Tidewave::FileTracker.write_file(path, content)
|
24
|
+
_meta[:mtime] = Time.now.to_i
|
25
|
+
|
26
|
+
"OK"
|
24
27
|
end
|
25
28
|
end
|
data/lib/tidewave/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tidewave
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yorick Jacquin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.
|
33
|
+
version: 1.5.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.
|
40
|
+
version: 1.5.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rack
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,14 +66,13 @@ files:
|
|
66
66
|
- lib/tidewave/configuration.rb
|
67
67
|
- lib/tidewave/file_tracker.rb
|
68
68
|
- lib/tidewave/railtie.rb
|
69
|
-
- lib/tidewave/tool_resolver.rb
|
70
69
|
- lib/tidewave/tools/base.rb
|
71
70
|
- lib/tidewave/tools/edit_project_file.rb
|
72
71
|
- lib/tidewave/tools/execute_sql_query.rb
|
73
72
|
- lib/tidewave/tools/get_logs.rb
|
74
73
|
- lib/tidewave/tools/get_models.rb
|
74
|
+
- lib/tidewave/tools/get_package_location.rb
|
75
75
|
- lib/tidewave/tools/get_source_location.rb
|
76
|
-
- lib/tidewave/tools/glob_project_files.rb
|
77
76
|
- lib/tidewave/tools/grep_project_files.rb
|
78
77
|
- lib/tidewave/tools/list_project_files.rb
|
79
78
|
- lib/tidewave/tools/package_search.rb
|
@@ -1,72 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "rack"
|
4
|
-
require "json"
|
5
|
-
require "active_support/core_ext/class"
|
6
|
-
|
7
|
-
gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
|
8
|
-
Dir[gem_tools_path].each { |f| require f }
|
9
|
-
|
10
|
-
module Tidewave
|
11
|
-
class ToolResolver
|
12
|
-
ALL_TOOLS = Tidewave::Tools::Base.descendants
|
13
|
-
NON_FILE_SYSTEM_TOOLS = ALL_TOOLS.reject(&:file_system_tool?)
|
14
|
-
MESSAGES_PATH = "/tidewave/messages".freeze
|
15
|
-
TOOLS_LIST_METHOD = "tools/list".freeze
|
16
|
-
INCLUDE_FS_TOOLS_PARAM = "include_fs_tools".freeze
|
17
|
-
|
18
|
-
def initialize(app, server)
|
19
|
-
@app = app
|
20
|
-
@server = server
|
21
|
-
end
|
22
|
-
|
23
|
-
def call(env)
|
24
|
-
request = Rack::Request.new(env)
|
25
|
-
request_path = request.path
|
26
|
-
request_body = extract_request_body(request)
|
27
|
-
request_params = request.params
|
28
|
-
|
29
|
-
# Override tools list response if requested
|
30
|
-
return override_tools_list_response(env) if overriding_tools_list_request?(request_path, request_params, request_body)
|
31
|
-
|
32
|
-
# Forward the request to the underlying app (RackTransport)
|
33
|
-
@app.call(env)
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def extract_request_body(request)
|
39
|
-
JSON.parse(request.body.read)
|
40
|
-
rescue JSON::ParserError => e
|
41
|
-
{}
|
42
|
-
ensure
|
43
|
-
request.body.rewind
|
44
|
-
end
|
45
|
-
|
46
|
-
# When we want to exclude file system tools, we need to handle the request differently to prevent from listing them
|
47
|
-
def overriding_tools_list_request?(request_path, request_params, request_body)
|
48
|
-
request_path == MESSAGES_PATH && request_body["method"] == TOOLS_LIST_METHOD && request_params[INCLUDE_FS_TOOLS_PARAM] != "true"
|
49
|
-
end
|
50
|
-
|
51
|
-
RESPONSE_HEADERS = { "Content-Type" => "application/json" }
|
52
|
-
|
53
|
-
def override_tools_list_response(env)
|
54
|
-
register_non_file_system_tools
|
55
|
-
@app.call(env).tap { register_all_tools }
|
56
|
-
end
|
57
|
-
|
58
|
-
def register_non_file_system_tools
|
59
|
-
reset_server_tools
|
60
|
-
@server.register_tools(*NON_FILE_SYSTEM_TOOLS)
|
61
|
-
end
|
62
|
-
|
63
|
-
def register_all_tools
|
64
|
-
reset_server_tools
|
65
|
-
@server.register_tools(*ALL_TOOLS)
|
66
|
-
end
|
67
|
-
|
68
|
-
def reset_server_tools
|
69
|
-
@server.instance_variable_set(:@tools, {})
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "tidewave/file_tracker"
|
4
|
-
|
5
|
-
class Tidewave::Tools::GlobProjectFiles < Tidewave::Tools::Base
|
6
|
-
file_system_tool
|
7
|
-
|
8
|
-
tool_name "glob_project_files"
|
9
|
-
description "Searches for files matching the given glob pattern."
|
10
|
-
|
11
|
-
arguments do
|
12
|
-
required(:pattern).filled(:string).description('The glob pattern to match files against, e.g., \"**/*.ex\"')
|
13
|
-
end
|
14
|
-
|
15
|
-
def call(pattern:)
|
16
|
-
Dir.glob(pattern, base: Tidewave::FileTracker.git_root)
|
17
|
-
end
|
18
|
-
end
|