tidewave 0.1.2 → 0.2.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 +25 -26
- data/lib/tidewave/configuration.rb +6 -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/file_tracker.rb +46 -67
- data/lib/tidewave/middleware.rb +182 -0
- data/lib/tidewave/railtie.rb +36 -27
- data/lib/tidewave/tools/base.rb +0 -7
- data/lib/tidewave/tools/edit_project_file.rb +6 -5
- data/lib/tidewave/tools/execute_sql_query.rb +2 -20
- data/lib/tidewave/tools/get_docs.rb +64 -0
- data/lib/tidewave/tools/get_models.rb +21 -10
- data/lib/tidewave/tools/get_package_location.rb +41 -0
- data/lib/tidewave/tools/get_source_location.rb +38 -35
- data/lib/tidewave/tools/list_project_files.rb +16 -4
- data/lib/tidewave/tools/project_eval.rb +49 -9
- 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
- data/lib/tidewave.rb +6 -3
- metadata +11 -8
- data/lib/tidewave/tool_resolver.rb +0 -72
- data/lib/tidewave/tools/glob_project_files.rb +0 -18
- data/lib/tidewave/tools/grep_project_files.rb +0 -108
- data/lib/tidewave/tools/package_search.rb +0 -36
data/lib/tidewave/railtie.rb
CHANGED
@@ -1,40 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fast_mcp"
|
4
3
|
require "logger"
|
5
4
|
require "fileutils"
|
6
|
-
require "tidewave/tool_resolver"
|
7
5
|
require "tidewave/configuration"
|
6
|
+
require "tidewave/middleware"
|
7
|
+
require "tidewave/exceptions_middleware"
|
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
|
11
|
-
config.tidewave = Tidewave::Configuration.new
|
12
|
-
|
13
|
-
initializer "tidewave.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
app
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
) do |server|
|
32
|
-
app.config.before_initialize do
|
33
|
-
# Register a custom middleware to register tools depending on `include_fs_tools` query parameter
|
34
|
-
server.register_tools(*Tidewave::ToolResolver::ALL_TOOLS)
|
35
|
-
app.middleware.use Tidewave::ToolResolver, server
|
14
|
+
config.tidewave = Tidewave::Configuration.new()
|
15
|
+
|
16
|
+
initializer "tidewave.setup" do |app|
|
17
|
+
unless app.config.enable_reloading
|
18
|
+
raise "For security reasons, Tidewave is only supported in environments where config.enable_reloading is true (typically development)"
|
19
|
+
end
|
20
|
+
|
21
|
+
app.config.middleware.insert_after(
|
22
|
+
ActionDispatch::Callbacks,
|
23
|
+
Tidewave::Middleware,
|
24
|
+
app.config.tidewave
|
25
|
+
)
|
26
|
+
|
27
|
+
app.config.after_initialize do
|
28
|
+
# If the user configured CSP, we need to alter it in dev
|
29
|
+
# to allow TC to run browser_eval.
|
30
|
+
app.config.content_security_policy.try do |content_security_policy|
|
31
|
+
content_security_policy.directives["script-src"].try do |script_src|
|
32
|
+
script_src << "'unsafe-eval'" unless script_src.include?("'unsafe-eval'")
|
33
|
+
end
|
36
34
|
end
|
37
35
|
end
|
38
36
|
end
|
37
|
+
|
38
|
+
initializer "tidewave.intercept_exceptions" do |app|
|
39
|
+
# We intercept exceptions from DebugExceptions, format the
|
40
|
+
# information as text and inject into the exception page html.
|
41
|
+
|
42
|
+
ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
|
43
|
+
request.set_header("tidewave.exception", exception)
|
44
|
+
end
|
45
|
+
|
46
|
+
app.middleware.insert_before(ActionDispatch::DebugExceptions, Tidewave::ExceptionsMiddleware)
|
47
|
+
end
|
39
48
|
end
|
40
49
|
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,13 +26,14 @@ 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
|
-
old_content = Tidewave::FileTracker.read_file(path)
|
36
|
+
_mtime, old_content = Tidewave::FileTracker.read_file(path)
|
36
37
|
|
37
38
|
# Ensure old_string is unique within the file
|
38
39
|
scan_result = old_content.scan(old_string)
|
@@ -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
|
@@ -3,7 +3,7 @@
|
|
3
3
|
class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
|
4
4
|
tool_name "execute_sql_query"
|
5
5
|
description <<~DESCRIPTION
|
6
|
-
Executes the given SQL query against the
|
6
|
+
Executes the given SQL query against the database connection.
|
7
7
|
Returns the result as a Ruby data structure.
|
8
8
|
|
9
9
|
Note that the output is limited to 50 rows at a time. If you need to see more, perform additional calls
|
@@ -25,24 +25,6 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
|
|
25
25
|
RESULT_LIMIT = 50
|
26
26
|
|
27
27
|
def call(query:, arguments: [])
|
28
|
-
|
29
|
-
conn = ActiveRecord::Base.connection
|
30
|
-
|
31
|
-
# Execute the query with prepared statement and arguments
|
32
|
-
if arguments.any?
|
33
|
-
result = conn.exec_query(query, "SQL", arguments)
|
34
|
-
else
|
35
|
-
result = conn.exec_query(query)
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
# Format the result
|
40
|
-
{
|
41
|
-
columns: result.columns,
|
42
|
-
rows: result.rows.first(RESULT_LIMIT),
|
43
|
-
row_count: result.rows.length,
|
44
|
-
adapter: conn.adapter_name,
|
45
|
-
database: Rails.configuration.database_configuration.dig(Rails.env, "database")
|
46
|
-
}
|
28
|
+
Tidewave::DatabaseAdapter.current.execute_query(query, arguments)
|
47
29
|
end
|
48
30
|
end
|
@@ -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
|
@@ -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 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
|
@@ -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,61 @@
|
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
def call(reference:)
|
24
|
+
file_path, line_number = self.class.get_source_location(reference)
|
25
|
+
|
26
|
+
if file_path
|
27
|
+
begin
|
28
|
+
relative_path = Pathname.new(file_path).relative_path_from(Rails.root)
|
29
|
+
"#{relative_path}:#{line_number}"
|
30
|
+
rescue ArgumentError
|
31
|
+
# If the path cannot be made relative, return the absolute path
|
32
|
+
"#{file_path}:#{line_number}"
|
33
|
+
end
|
34
|
+
else
|
35
|
+
raise NameError, "could not find source location for #{reference}"
|
36
|
+
end
|
31
37
|
end
|
32
38
|
|
33
|
-
|
39
|
+
def self.get_source_location(reference)
|
40
|
+
constant_path, selector, method_name = reference.rpartition(/\.|#/)
|
41
|
+
|
42
|
+
# There are no selectors, so the method_name is a constant path
|
43
|
+
return Object.const_source_location(method_name) if selector.empty?
|
34
44
|
|
35
|
-
def get_source_location(module_name, function_name)
|
36
45
|
begin
|
37
|
-
|
38
|
-
rescue NameError
|
39
|
-
raise
|
46
|
+
mod = Object.const_get(constant_path)
|
47
|
+
rescue NameError => e
|
48
|
+
raise e
|
49
|
+
rescue
|
50
|
+
raise "wrong or invalid reference #{reference}"
|
40
51
|
end
|
41
52
|
|
42
|
-
|
53
|
+
raise "reference #{constant_path} does not point a class/module" unless mod.is_a?(Module)
|
43
54
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
rescue NameError
|
50
|
-
get_instance_method_definition(module_ref, function_name)
|
51
|
-
end
|
52
|
-
|
53
|
-
def get_instance_method_definition(module_ref, function_name)
|
54
|
-
module_ref.instance_method(function_name).source_location
|
55
|
-
rescue NameError
|
56
|
-
raise NameError, "Method #{function_name} not found in module #{module_ref.name}"
|
55
|
+
if selector == "#"
|
56
|
+
mod.instance_method(method_name).source_location
|
57
|
+
else
|
58
|
+
mod.method(method_name).source_location
|
59
|
+
end
|
57
60
|
end
|
58
61
|
end
|
@@ -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.
|
16
|
+
DESC
|
17
|
+
|
18
|
+
arguments do
|
19
|
+
optional(:glob_pattern).maybe(:string).description("Optional: a glob pattern to filter the listed files")
|
20
|
+
optional(:include_ignored).maybe(:bool).description("Optional: whether to include files that are ignored by .gitignore. Defaults to false. WARNING: Use with targeted glob patterns to avoid listing excessive files from dependencies or build directories.")
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(glob_pattern: nil, include_ignored: false)
|
24
|
+
Tidewave::FileTracker.project_files(glob_pattern: glob_pattern, include_ignored: include_ignored)
|
13
25
|
end
|
14
26
|
end
|
@@ -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).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
|
@@ -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
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
|