tidewave 0.4.2 → 0.5.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 +10 -2
- data/lib/tidewave/configuration.rb +3 -2
- data/lib/tidewave/database_adapter.rb +2 -7
- data/lib/tidewave/database_adapters/active_record.rb +2 -8
- data/lib/tidewave/database_adapters/sequel.rb +14 -2
- data/lib/tidewave/railtie.rb +14 -29
- data/lib/tidewave/tool.rb +128 -0
- data/lib/tidewave/tools/execute_sql_query.rb +31 -14
- data/lib/tidewave/tools/get_docs.rb +26 -8
- data/lib/tidewave/tools/get_logs.rb +38 -10
- data/lib/tidewave/tools/get_models.rb +37 -18
- data/lib/tidewave/tools/get_source_location.rb +50 -37
- data/lib/tidewave/tools/project_eval.rb +43 -21
- data/lib/tidewave/version.rb +2 -2
- data/lib/tidewave.rb +312 -5
- metadata +3 -33
- data/lib/tidewave/middleware.rb +0 -127
- data/lib/tidewave/streamable_http_transport.rb +0 -135
- data/lib/tidewave/tools/base.rb +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d75b5659655095637ad8830051ef50f06e018339520d093c51854cec9c320fd7
|
|
4
|
+
data.tar.gz: 9325b6a4ea7ac4e1444a677bd1dc785bc6f8046e8492a5d1b70267c9424ca9a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fe7d353f945a3a3735dfdd56486188df12034736c2ac78561dca478b05b6f15fba9608dd8e8209f3d9a66659f4908c2814ec65ccdf13030f56b1387a9190fde
|
|
7
|
+
data.tar.gz: 6782f79c55813d99d5cffeb973812116aaf581570119de0ebcd739d2f65d1aa0fbc3ed3bc1cc1d478ec0b30ac18fbc2ab9db6037df18ec433b8ef50cce7eb947
|
data/README.md
CHANGED
|
@@ -20,6 +20,14 @@ gem "tidewave", group: :development
|
|
|
20
20
|
|
|
21
21
|
Now make sure [Tidewave is installed](https://hexdocs.pm/tidewave/installation.html) and you are ready to connect Tidewave to your app.
|
|
22
22
|
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
Run the Minitest suite with:
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
bundle exec ruby -Itest test/all_test.rb
|
|
29
|
+
```
|
|
30
|
+
|
|
23
31
|
## Troubleshooting
|
|
24
32
|
|
|
25
33
|
### Using multiple hosts/subdomains
|
|
@@ -58,7 +66,7 @@ You may configure `tidewave` using the following syntax:
|
|
|
58
66
|
|
|
59
67
|
The following config is available:
|
|
60
68
|
|
|
61
|
-
* `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces.
|
|
69
|
+
* `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces, for security purposes. Read [our security guidelines for more information and when to allow remote access](https://hexdocs.pm/tidewave/security.html) (if you know what you are doing)
|
|
62
70
|
|
|
63
71
|
* `logger_middleware` - The logger middleware Tidewave should wrap to silence its own logs
|
|
64
72
|
|
|
@@ -88,7 +96,7 @@ The following config is available:
|
|
|
88
96
|
|
|
89
97
|
## Acknowledgements
|
|
90
98
|
|
|
91
|
-
A thank you to Yorick Jacquin
|
|
99
|
+
A thank you to Yorick Jacquin for the initial version of this project.
|
|
92
100
|
|
|
93
101
|
## License
|
|
94
102
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
class Tidewave
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :logger, :allow_remote_access, :preferred_orm, :dev, :client_url, :team, :logger_middleware
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
|
-
|
|
8
|
+
# Rails has a hosts middleware which already checks for this
|
|
9
9
|
@allow_remote_access = true
|
|
10
|
+
@logger = nil
|
|
10
11
|
@preferred_orm = :active_record
|
|
11
12
|
@dev = false
|
|
12
13
|
@client_url = "https://tidewave.ai"
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
class Tidewave
|
|
4
4
|
class DatabaseAdapter
|
|
5
5
|
class << self
|
|
6
|
-
def
|
|
7
|
-
@current ||= create_adapter
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def create_adapter
|
|
11
|
-
orm_type = Rails.application.config.tidewave.preferred_orm
|
|
6
|
+
def for(orm_type)
|
|
12
7
|
case orm_type
|
|
13
8
|
when :active_record
|
|
14
9
|
require_relative "database_adapters/active_record"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
class Tidewave
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class ActiveRecord < DatabaseAdapter
|
|
6
6
|
RESULT_LIMIT = 50
|
|
@@ -21,19 +21,13 @@ module Tidewave
|
|
|
21
21
|
rows: result.rows.first(RESULT_LIMIT),
|
|
22
22
|
row_count: result.rows.length,
|
|
23
23
|
adapter: conn.adapter_name,
|
|
24
|
-
database:
|
|
24
|
+
database: conn.pool.db_config.database
|
|
25
25
|
}
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def get_models
|
|
29
29
|
::ActiveRecord::Base.descendants
|
|
30
30
|
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def database_name
|
|
35
|
-
Rails.configuration.database_configuration.dig(Rails.env, "database")
|
|
36
|
-
end
|
|
37
31
|
end
|
|
38
32
|
end
|
|
39
33
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
class Tidewave
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class Sequel < DatabaseAdapter
|
|
6
6
|
RESULT_LIMIT = 50
|
|
@@ -31,7 +31,19 @@ module Tidewave
|
|
|
31
31
|
|
|
32
32
|
def get_models
|
|
33
33
|
# Filter out anonymous Sequel models that can't be resolved as constants
|
|
34
|
-
::Sequel::Model.
|
|
34
|
+
descendants_of(::Sequel::Model).reject { |model| model.name&.start_with?("Sequel::_Model(") }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def descendants_of(base)
|
|
40
|
+
if base.respond_to?(:descendants)
|
|
41
|
+
base.descendants
|
|
42
|
+
elsif base.respond_to?(:subclasses)
|
|
43
|
+
base.subclasses.flat_map { |subclass| [ subclass ] + descendants_of(subclass) }
|
|
44
|
+
else
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
35
47
|
end
|
|
36
48
|
end
|
|
37
49
|
end
|
data/lib/tidewave/railtie.rb
CHANGED
|
@@ -1,37 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
|
-
require "fileutils"
|
|
5
4
|
require "tidewave/configuration"
|
|
6
|
-
require "tidewave/middleware"
|
|
7
5
|
require "tidewave/exceptions_middleware"
|
|
8
6
|
require "tidewave/quiet_requests_middleware"
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
Dir[gem_tools_path].each { |f| require f }
|
|
12
|
-
|
|
13
|
-
# Temporary monkey patching to address regression in FastMCP
|
|
14
|
-
if Dry::Schema::Macros::Hash.method_defined?(:original_call)
|
|
15
|
-
Dry::Schema::Macros::Hash.class_eval do
|
|
16
|
-
def call(*args, &block)
|
|
17
|
-
if block
|
|
18
|
-
# Use current context to track nested context if available
|
|
19
|
-
context = MetadataContext.current
|
|
20
|
-
if context
|
|
21
|
-
context.with_nested(name) do
|
|
22
|
-
original_call(*args, &block)
|
|
23
|
-
end
|
|
24
|
-
else
|
|
25
|
-
original_call(*args, &block)
|
|
26
|
-
end
|
|
27
|
-
else
|
|
28
|
-
original_call(*args)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
module Tidewave
|
|
8
|
+
class Tidewave
|
|
35
9
|
class Railtie < Rails::Railtie
|
|
36
10
|
config.tidewave = Tidewave::Configuration.new()
|
|
37
11
|
|
|
@@ -40,10 +14,21 @@ module Tidewave
|
|
|
40
14
|
raise "For security reasons, Tidewave is only supported in environments where config.enable_reloading is true (typically development)"
|
|
41
15
|
end
|
|
42
16
|
|
|
17
|
+
tidewave_config = app.config.tidewave
|
|
18
|
+
|
|
43
19
|
app.config.middleware.insert_after(
|
|
44
20
|
ActionDispatch::Callbacks,
|
|
45
|
-
Tidewave
|
|
46
|
-
|
|
21
|
+
Tidewave,
|
|
22
|
+
allow_remote_access: tidewave_config.allow_remote_access,
|
|
23
|
+
client_url: tidewave_config.client_url,
|
|
24
|
+
framework_type: "rails",
|
|
25
|
+
project_name: app.class.module_parent.name,
|
|
26
|
+
team: tidewave_config.team,
|
|
27
|
+
logger: tidewave_config.logger || Rails.logger,
|
|
28
|
+
root: Rails.root,
|
|
29
|
+
log_file: Rails.root.join("log", "#{Rails.env}.log"),
|
|
30
|
+
orm_adapter: tidewave_config.preferred_orm,
|
|
31
|
+
before_reload: -> { app.eager_load! }
|
|
47
32
|
)
|
|
48
33
|
|
|
49
34
|
app.config.after_initialize do
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tidewave
|
|
4
|
+
class Tool
|
|
5
|
+
class << self
|
|
6
|
+
def descendants
|
|
7
|
+
@descendants ||= []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def inherited(subclass)
|
|
11
|
+
descendants << subclass
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(_options = {})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def definition
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #definition"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(_arguments = {})
|
|
24
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_and_call(arguments)
|
|
28
|
+
arguments ||= {}
|
|
29
|
+
|
|
30
|
+
unless arguments.is_a?(Hash)
|
|
31
|
+
raise ArgumentError, "Invalid arguments: expected an object"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# This validator intentionally enforces only a small subset of JSON Schema:
|
|
35
|
+
# `type`, `required`, and scalar `default` values. Other keywords such as
|
|
36
|
+
# `minLength`, `maxLength`, `enum`, and `pattern` remain descriptive until
|
|
37
|
+
# Tidewave grows broader schema support.
|
|
38
|
+
validate_schema(arguments, definition.fetch("inputSchema", {}))
|
|
39
|
+
call(arguments)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def validate_schema(value, schema, path = nil)
|
|
45
|
+
return unless schema.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
validate_type(value, schema["type"], path) if schema["type"]
|
|
48
|
+
|
|
49
|
+
case schema["type"]
|
|
50
|
+
when "object"
|
|
51
|
+
validate_object(value, schema, path)
|
|
52
|
+
when "array"
|
|
53
|
+
validate_array(value, schema, path)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_object(value, schema, path)
|
|
58
|
+
properties = schema.fetch("properties", {})
|
|
59
|
+
|
|
60
|
+
properties.each do |name, property_schema|
|
|
61
|
+
if !value.key?(name) && property_schema.is_a?(Hash) && property_schema.key?("default")
|
|
62
|
+
validate_default(property_schema["default"], property_path(path, name))
|
|
63
|
+
value[name] = property_schema["default"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
next unless value.key?(name)
|
|
67
|
+
|
|
68
|
+
validate_schema(value[name], property_schema, property_path(path, name))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
validate_required_properties(value, schema.fetch("required", []), path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_array(value, schema, path)
|
|
75
|
+
item_schema = schema["items"]
|
|
76
|
+
return unless item_schema.is_a?(Hash) && !item_schema.empty?
|
|
77
|
+
|
|
78
|
+
value.each_with_index do |item, index|
|
|
79
|
+
validate_schema(item, item_schema, "#{path || 'value'}[#{index}]")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_required_properties(value, required_properties, path)
|
|
84
|
+
required_properties.each do |name|
|
|
85
|
+
next if value.key?(name)
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "Invalid arguments: missing required property '#{property_path(path, name)}'"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_type(value, type, path)
|
|
92
|
+
return if value_matches_type?(value, type)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "Invalid arguments: property '#{path || 'value'}' must be #{article_for(type)} #{type}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def value_matches_type?(value, type)
|
|
98
|
+
case type
|
|
99
|
+
when "object"
|
|
100
|
+
value.is_a?(Hash)
|
|
101
|
+
when "array"
|
|
102
|
+
value.is_a?(Array)
|
|
103
|
+
when "string"
|
|
104
|
+
value.is_a?(String)
|
|
105
|
+
when "integer"
|
|
106
|
+
value.is_a?(Integer)
|
|
107
|
+
when "boolean"
|
|
108
|
+
value == true || value == false
|
|
109
|
+
else
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def property_path(path, name)
|
|
115
|
+
path ? "#{path}.#{name}" : name
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def article_for(type)
|
|
119
|
+
%w[array integer object].include?(type) ? "an" : "a"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_default(value, path)
|
|
123
|
+
return unless value.is_a?(Hash) || value.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
raise ArgumentError, "Invalid tool definition: property '#{path}' cannot use an object or array default"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Tidewave::Tools::ExecuteSqlQuery < Tidewave::
|
|
4
|
-
|
|
5
|
-
description <<~DESCRIPTION
|
|
3
|
+
class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tool
|
|
4
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
6
5
|
Executes the given SQL query against the database connection.
|
|
7
6
|
Returns the result as a Ruby data structure.
|
|
8
7
|
|
|
@@ -17,20 +16,38 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
|
|
|
17
16
|
For MySQL, use ? for parameter placeholders.
|
|
18
17
|
DESCRIPTION
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
optional(:arguments).value(:array).description("The arguments to pass to the query. The query must contain corresponding parameter placeholders.")
|
|
19
|
+
def initialize(options = {})
|
|
20
|
+
@database_adapter = Tidewave::DatabaseAdapter.for(options[:orm_adapter]) if options[:orm_adapter]
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
def definition
|
|
24
|
+
return nil unless @database_adapter
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"name" => "execute_sql_query",
|
|
28
|
+
"description" => DESCRIPTION,
|
|
29
|
+
"inputSchema" => {
|
|
30
|
+
"type" => "object",
|
|
31
|
+
"properties" => {
|
|
32
|
+
"query" => {
|
|
33
|
+
"type" => "string",
|
|
34
|
+
"minLength" => 1,
|
|
35
|
+
"description" => "The SQL query to execute. For PostgreSQL, use $1, $2 placeholders. For MySQL, use ? placeholders."
|
|
36
|
+
},
|
|
37
|
+
"arguments" => {
|
|
38
|
+
"type" => "array",
|
|
39
|
+
"items" => {},
|
|
40
|
+
"description" => "The arguments to pass to the query. The query must contain corresponding parameter placeholders."
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"required" => [ "query" ]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
29
46
|
end
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
def call(arguments_hash)
|
|
49
|
+
query = arguments_hash.fetch("query")
|
|
50
|
+
arguments = arguments_hash.fetch("arguments", [])
|
|
51
|
+
@database_adapter.execute_query(query, arguments)
|
|
35
52
|
end
|
|
36
53
|
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Tidewave::Tools::GetDocs < Tidewave::
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
description <<~DESCRIPTION
|
|
3
|
+
class Tidewave::Tools::GetDocs < Tidewave::Tool
|
|
4
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
7
5
|
Returns the documentation for the given reference.
|
|
8
6
|
|
|
9
7
|
The reference may be a constant, most commonly classes and modules
|
|
@@ -16,11 +14,26 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
|
|
|
16
14
|
If that is the case, prefer this tool over grepping the file system.
|
|
17
15
|
DESCRIPTION
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
def definition
|
|
18
|
+
{
|
|
19
|
+
"name" => "get_docs",
|
|
20
|
+
"description" => DESCRIPTION,
|
|
21
|
+
"inputSchema" => {
|
|
22
|
+
"type" => "object",
|
|
23
|
+
"properties" => {
|
|
24
|
+
"reference" => {
|
|
25
|
+
"type" => "string",
|
|
26
|
+
"minLength" => 1,
|
|
27
|
+
"description" => "The constant/method to lookup, such String, String#gsub or File.executable?"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required" => [ "reference" ]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
end
|
|
22
34
|
|
|
23
|
-
def call(
|
|
35
|
+
def call(arguments)
|
|
36
|
+
reference = arguments.fetch("reference")
|
|
24
37
|
file_path, line_number = Tidewave::Tools::GetSourceLocation.get_source_location(reference)
|
|
25
38
|
|
|
26
39
|
if file_path
|
|
@@ -47,7 +60,8 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
|
|
|
47
60
|
line = lines[current_line].chomp.strip
|
|
48
61
|
|
|
49
62
|
if line.start_with?("#")
|
|
50
|
-
|
|
63
|
+
comment = line.sub(/^#\s|^#/, "")
|
|
64
|
+
comment_lines.unshift(comment) unless ignorable_comment?(comment)
|
|
51
65
|
elsif line.empty?
|
|
52
66
|
# Skip empty lines but continue looking for comments
|
|
53
67
|
else
|
|
@@ -61,4 +75,8 @@ class Tidewave::Tools::GetDocs < Tidewave::Tools::Base
|
|
|
61
75
|
return nil if comment_lines.empty?
|
|
62
76
|
comment_lines.join("\n")
|
|
63
77
|
end
|
|
78
|
+
|
|
79
|
+
def ignorable_comment?(comment)
|
|
80
|
+
comment.start_with?("rubocop:")
|
|
81
|
+
end
|
|
64
82
|
end
|
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
class Tidewave::Tools::GetLogs < Tidewave::Tool
|
|
6
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
6
7
|
Returns all log output, excluding logs that were caused by other tool calls.
|
|
7
8
|
|
|
8
9
|
Use this tool to check for request logs or potentially logged errors.
|
|
9
10
|
DESCRIPTION
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
@log_file = options[:log_file] ? Pathname.new(options[:log_file].to_s) : nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def definition
|
|
17
|
+
return nil unless @log_file
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
"name" => "get_logs",
|
|
21
|
+
"description" => DESCRIPTION,
|
|
22
|
+
"inputSchema" => {
|
|
23
|
+
"type" => "object",
|
|
24
|
+
"properties" => {
|
|
25
|
+
"tail" => {
|
|
26
|
+
"type" => "integer",
|
|
27
|
+
"not" => {
|
|
28
|
+
"type" => "null"
|
|
29
|
+
},
|
|
30
|
+
"description" => "The number of log entries to return from the end of the log"
|
|
31
|
+
},
|
|
32
|
+
"grep" => {
|
|
33
|
+
"type" => "string",
|
|
34
|
+
"minLength" => 1,
|
|
35
|
+
"description" => "Filter logs with the given regular expression (case insensitive). E.g. \"error\" when you want to capture errors in particular"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required" => [ "tail" ]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
14
41
|
end
|
|
15
42
|
|
|
16
|
-
def call(
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
def call(arguments)
|
|
44
|
+
tail = arguments.fetch("tail")
|
|
45
|
+
grep = arguments["grep"]
|
|
46
|
+
return "Log file not found" unless @log_file&.exist?
|
|
19
47
|
|
|
20
48
|
regex = Regexp.new(grep, Regexp::IGNORECASE) if grep
|
|
21
49
|
matching_lines = []
|
|
22
50
|
|
|
23
|
-
tail_lines(log_file) do |line|
|
|
51
|
+
tail_lines(@log_file) do |line|
|
|
24
52
|
if regex.nil? || line.match?(regex)
|
|
25
53
|
matching_lines.unshift(line)
|
|
26
54
|
break if matching_lines.size >= tail
|
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
class Tidewave::Tools::GetModels < Tidewave::Tool
|
|
6
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
6
7
|
Returns a list of all database-backed models in the application.
|
|
7
8
|
DESCRIPTION
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@root = options[:root] ? Pathname.new(options[:root].to_s) : Pathname.pwd
|
|
12
|
+
@database_adapter = Tidewave::DatabaseAdapter.for(options[:orm_adapter]) if options[:orm_adapter]
|
|
13
|
+
@before_reload = options[:before_reload]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def definition
|
|
17
|
+
return nil unless @database_adapter
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
"name" => "get_models",
|
|
21
|
+
"description" => DESCRIPTION,
|
|
22
|
+
"inputSchema" => {
|
|
23
|
+
"type" => "object",
|
|
24
|
+
"properties" => {}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(_arguments)
|
|
30
|
+
@before_reload&.call
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
models = Tidewave::DatabaseAdapter.current.get_models
|
|
32
|
+
models = @database_adapter.get_models
|
|
15
33
|
|
|
16
34
|
models.map do |model|
|
|
35
|
+
display_name = model.name || model.to_s
|
|
36
|
+
|
|
17
37
|
if location = get_relative_source_location(model.name)
|
|
18
|
-
"* #{
|
|
38
|
+
"* #{display_name} at #{location}"
|
|
19
39
|
else
|
|
20
|
-
"* #{
|
|
40
|
+
"* #{display_name}"
|
|
21
41
|
end
|
|
22
42
|
end.join("\n")
|
|
23
43
|
end
|
|
@@ -25,16 +45,15 @@ class Tidewave::Tools::GetModels < Tidewave::Tools::Base
|
|
|
25
45
|
private
|
|
26
46
|
|
|
27
47
|
def get_relative_source_location(model_name)
|
|
48
|
+
return nil if model_name.nil? || model_name.empty?
|
|
49
|
+
|
|
28
50
|
source_location = Object.const_source_location(model_name)
|
|
29
|
-
return nil
|
|
51
|
+
return nil unless source_location
|
|
30
52
|
|
|
31
53
|
file_path, line_number = source_location
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# If the path cannot be made relative, return the absolute path
|
|
37
|
-
"#{file_path}:#{line_number}"
|
|
38
|
-
end
|
|
54
|
+
relative_path = Pathname.new(file_path).relative_path_from(@root)
|
|
55
|
+
"#{relative_path}:#{line_number}"
|
|
56
|
+
rescue ArgumentError
|
|
57
|
+
"#{file_path}:#{line_number}"
|
|
39
58
|
end
|
|
40
59
|
end
|