tidewave 0.4.1 → 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 +54 -17
- 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 -192
- data/lib/tidewave/streamable_http_transport.rb +0 -131
- 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
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Tidewave is the coding agent for full-stack web app development. Integrate Claude Code, OpenAI Codex, and other agents with your web app and web framework at every layer, from UI to database. [See our website](https://tidewave.ai) for more information.
|
|
4
4
|
|
|
5
|
-
This project can also be used as a standalone Model Context Protocol server
|
|
5
|
+
This project can also be used as [a standalone Model Context Protocol server](https://hexdocs.pm/tidewave/mcp.html).
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
You can install Tidewave by
|
|
9
|
+
You can install Tidewave by running:
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
bundle add tidewave --group development
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
or by manully adding the `tidewave` gem to the development group in your Gemfile:
|
|
10
16
|
|
|
11
17
|
```ruby
|
|
12
18
|
gem "tidewave", group: :development
|
|
@@ -14,30 +20,41 @@ gem "tidewave", group: :development
|
|
|
14
20
|
|
|
15
21
|
Now make sure [Tidewave is installed](https://hexdocs.pm/tidewave/installation.html) and you are ready to connect Tidewave to your app.
|
|
16
22
|
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
Run the Minitest suite with:
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
bundle exec ruby -Itest test/all_test.rb
|
|
29
|
+
```
|
|
30
|
+
|
|
17
31
|
## Troubleshooting
|
|
18
32
|
|
|
19
|
-
###
|
|
33
|
+
### Using multiple hosts/subdomains
|
|
20
34
|
|
|
21
|
-
If you
|
|
35
|
+
If you are using multiple hosts/subdomains during development, you must use `*.localhost`, as such domains are considered secure by browsers. Additionally, add the following to `config/initializers/development.rb`:
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
```ruby
|
|
38
|
+
config.session_store :cookie_store,
|
|
39
|
+
key: "__your_app_session",
|
|
40
|
+
same_site: :none,
|
|
41
|
+
secure: true,
|
|
42
|
+
assume_ssl: true
|
|
43
|
+
```
|
|
24
44
|
|
|
25
|
-
|
|
45
|
+
And make sure you are using `rack-session` version `2.1.0` or later.
|
|
26
46
|
|
|
27
|
-
|
|
47
|
+
The above will allow your application to run embedded within Tidewave across multiple subdomains, as long as it is using a secure context (such as `admin.localhost`, `www.foobar.localhost`, etc).
|
|
28
48
|
|
|
29
|
-
###
|
|
49
|
+
### Content security policy
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly. It also disables the `frame-ancestors` directive.
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
### Production Environment
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
config.hosts << "company.local"
|
|
37
|
-
config.tidewave.allow_remote_access = true
|
|
38
|
-
```
|
|
55
|
+
Tidewave is a powerful tool that can help you develop your web application faster and more efficiently. However, it is important to note that Tidewave is not meant to be used in a production environment.
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
|
|
41
58
|
|
|
42
59
|
## Configuration
|
|
43
60
|
|
|
@@ -49,7 +66,7 @@ You may configure `tidewave` using the following syntax:
|
|
|
49
66
|
|
|
50
67
|
The following config is available:
|
|
51
68
|
|
|
52
|
-
* `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)
|
|
53
70
|
|
|
54
71
|
* `logger_middleware` - The logger middleware Tidewave should wrap to silence its own logs
|
|
55
72
|
|
|
@@ -57,9 +74,29 @@ The following config is available:
|
|
|
57
74
|
|
|
58
75
|
* `team` - set your Tidewave Team configuration, such as `config.tidewave.team = { id: "my-company" }`
|
|
59
76
|
|
|
77
|
+
## Available tools
|
|
78
|
+
|
|
79
|
+
- `execute_sql_query` - executes a SQL query within your application
|
|
80
|
+
database, useful for the agent to verify the result of an action
|
|
81
|
+
|
|
82
|
+
- `get_docs` - get the documentation for a given module/class/method.
|
|
83
|
+
It consults the exact versions used by the project, ensuring you always
|
|
84
|
+
get correct information
|
|
85
|
+
|
|
86
|
+
- `get_logs` - reads logs written by the server
|
|
87
|
+
|
|
88
|
+
- `get_models` - lists all modules in the application and their location
|
|
89
|
+
for quick discovery
|
|
90
|
+
|
|
91
|
+
- `get_source_location` - get the source location for a given module/class/method,
|
|
92
|
+
so an agent can directly read the source skipping search
|
|
93
|
+
|
|
94
|
+
- `project_eval` - evaluates code within the Rails application itself, giving the agent
|
|
95
|
+
access to your runtime, dependencies, and in-memory data
|
|
96
|
+
|
|
60
97
|
## Acknowledgements
|
|
61
98
|
|
|
62
|
-
A thank you to Yorick Jacquin
|
|
99
|
+
A thank you to Yorick Jacquin for the initial version of this project.
|
|
63
100
|
|
|
64
101
|
## License
|
|
65
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
|