tidewave 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d708afbfcf6d89cc1456bd7bcefdc660952571cda0c6635b8c6b14b8fa31d91e
4
- data.tar.gz: da923b2f69608fe95eb11f76795cef1598f9cc24941afe50081afc4b76c66cfa
3
+ metadata.gz: d140360315f54166fc14e330f33fc5dd62e561c49ba3d6cb5044a8e7b5247e41
4
+ data.tar.gz: c69a1a0363ce0f50a396599f2b551896a602431929731dd9fbf4fbe283823cf1
5
5
  SHA512:
6
- metadata.gz: 9b0eaa2a77690e4dda1720e5c2c84b02ea93d9b69955e2e430745da483d30014ba3fc5d59dc40c92b0ccdedd4413f4198012813a33f2332fd7ca08a9fac79fc1
7
- data.tar.gz: 501103bf39a69932a408ebf9e455640fc8fb97a507163d6b4f42faf27aead8811927ecc249858978c8c5a46213dbe5b1d8635ade60817f546645ec99fa4c203a
6
+ metadata.gz: 6f3203bbce1c31681fcb2b21575ea62a8714a469f61ec30be8d59ccd22028592b7a33317b4f434e0d7a4051caaf1db1aa82247d395a961d4a09f1a1538cc6cd8
7
+ data.tar.gz: 968742eed4690b5c2a1f8c0ecf233e4742a19bc2ac8a700d4f64eca2878bea8f70d1d2228cffc472019462790eb122601b7acfc608fd7950c100974f3c9053d4
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # Tidewave
2
2
 
3
- Tidewave speeds up development with an AI assistant that understands your web application,
4
- how it runs, and what it delivers. Our current release connects your editor's
5
- assistant to your web framework runtime via [MCP](https://modelcontextprotocol.io/).
3
+ Tidewave is the coding agent for full-stack web app development, deeply integrated with Rails, from the database to the UI. [See our website](https://tidewave.ai) for more information.
6
4
 
7
- [See our website](https://tidewave.ai) for more information.
5
+ This project can also be used as a standalone Model Context Protocol server for your editors.
8
6
 
9
7
  ## Installation
10
8
 
@@ -14,9 +12,7 @@ You can install Tidewave by adding the `tidewave` gem to the development group i
14
12
  gem "tidewave", group: :development
15
13
  ```
16
14
 
17
- Tidewave will now run on the same port as your regular Rails application.
18
- In particular, the MCP is located by default at http://localhost:3000/tidewave/mcp.
19
- [You must configure your editor and AI assistants accordingly](https://hexdocs.pm/tidewave/mcp.html).
15
+ Now access `/tidewave` route of your web application to enjoy Tidewave Web!
20
16
 
21
17
  ## Troubleshooting
22
18
 
@@ -29,22 +25,24 @@ config.hosts << "company.local"
29
25
  config.tidewave.allow_remote_access = true
30
26
  ```
31
27
 
32
- If you want to use Docker for development, you either need to enable the configuration above or automatically redirect the relevant ports, as done by [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers). See our [containars](https://hexdocs.pm/tidewave/containers.html) guide for more information.
28
+ If you want to use Docker for development, you either need to enable the configuration above or automatically redirect the relevant ports, as done by [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers). See our [containers](https://hexdocs.pm/tidewave/containers.html) guide for more information.
33
29
 
34
30
  ### Content security policy
35
31
 
36
32
  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.
37
33
 
34
+ ### Web server requirements
35
+
36
+ At the moment, Tidewave requires all requests to be processed by the same process. In case Tidewave cannot connect to your application, consider starting your Rails application as follows:
37
+
38
+ RAILS_MAX_THREADS=1 WEB_CONCURRENCY=1 rails server
39
+
38
40
  ### Production Environment
39
41
 
40
42
  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.
41
43
 
42
44
  Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
43
45
 
44
- ### Web server requirements
45
-
46
- Tidewave currently requires a threaded web server like Puma.
47
-
48
46
  ## Configuration
49
47
 
50
48
  You may configure `tidewave` using the following syntax:
@@ -53,11 +51,13 @@ You may configure `tidewave` using the following syntax:
53
51
  config.tidewave.allow_remote_access = true
54
52
  ```
55
53
 
56
- The following options are available:
54
+ The following config is available:
55
+
56
+ * `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces as well. If you trust your network and need to access Tidewave from a different machine, this configuration can be set to `true`
57
57
 
58
- * `:allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces as well. If you trust your network and need to access Tidewave from a different machine, this configuration can be set to `true`
58
+ * `preferred_orm` - which ORM to use, either `:active_record` (default) or `:sequel`
59
59
 
60
- * `:preferred_orm` - which ORM to use, either `:active_record` (default) or `:sequel`
60
+ * `team` - set your team configuration, such as `config.tidewave.team = { id: "my-company }`
61
61
 
62
62
  ## Acknowledgements
63
63
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tidewave
4
4
  class Configuration
5
- attr_accessor :logger, :allow_remote_access, :preferred_orm, :credentials, :client_url
5
+ attr_accessor :logger, :allow_remote_access, :preferred_orm, :credentials, :client_url, :team
6
6
 
7
7
  def initialize
8
8
  @logger = nil
@@ -10,6 +10,7 @@ module Tidewave
10
10
  @preferred_orm = :active_record
11
11
  @credentials = {}
12
12
  @client_url = "https://tidewave.ai"
13
+ @team = {}
13
14
  end
14
15
  end
15
16
  end
@@ -25,6 +25,7 @@ class Tidewave::Middleware
25
25
  def initialize(app, config)
26
26
  @allow_remote_access = config.allow_remote_access
27
27
  @client_url = config.client_url
28
+ @team = config.team
28
29
  @project_name = Rails.application.class.module_parent.name
29
30
 
30
31
  @app = FastMcp.rack_middleware(app,
@@ -78,7 +79,8 @@ class Tidewave::Middleware
78
79
  config = {
79
80
  "project_name" => @project_name,
80
81
  "framework_type" => "rails",
81
- "tidewave_version" => Tidewave::VERSION
82
+ "tidewave_version" => Tidewave::VERSION,
83
+ "team" => @team
82
84
  }
83
85
 
84
86
  html = <<~HTML
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tidewave::QuietRequestsMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env["PATH_INFO"].start_with?("/tidewave")
10
+ Rails.logger.silence { @app.call(env) }
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "tidewave/configuration"
6
6
  require "tidewave/middleware"
7
7
  require "tidewave/exceptions_middleware"
8
+ require "tidewave/quiet_requests_middleware"
8
9
 
9
10
  gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
10
11
  Dir[gem_tools_path].each { |f| require f }
@@ -45,5 +46,10 @@ module Tidewave
45
46
 
46
47
  app.middleware.insert_before(ActionDispatch::DebugExceptions, Tidewave::ExceptionsMiddleware)
47
48
  end
49
+
50
+ initializer "tidewave.logging" do |app|
51
+ # Do not pollute user logs with tidewave requests.
52
+ app.middleware.insert_before(Rails::Rack::Logger, Tidewave::QuietRequestsMiddleware)
53
+ end
48
54
  end
49
55
  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).last(tail)
20
- logs.join
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,7 +3,7 @@
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 +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 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.
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,6 +22,12 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
21
22
  end
22
23
 
23
24
  def call(reference:)
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
+
24
31
  file_path, line_number = self.class.get_source_location(reference)
25
32
 
26
33
  if file_path
@@ -36,6 +43,18 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
36
43
  end
37
44
  end
38
45
 
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
57
+
39
58
  def self.get_source_location(reference)
40
59
  constant_path, selector, method_name = reference.rpartition(/\.|#/)
41
60
 
@@ -20,7 +20,7 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
20
20
  required(:code).filled(:string).description("The Ruby code to evaluate")
21
21
  optional(:arguments).value(:array).description("The arguments to pass to evaluation. They are available inside the evaluated code as `arguments`.")
22
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.")
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.")
24
24
  end
25
25
 
26
26
  def call(code:, arguments: [], timeout: 30_000, json: false)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tidewave
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  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.2.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-08-12 00:00:00.000000000 Z
12
+ date: 2025-09-08 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rails
@@ -68,22 +69,16 @@ files:
68
69
  - lib/tidewave/database_adapters/active_record.rb
69
70
  - lib/tidewave/database_adapters/sequel.rb
70
71
  - lib/tidewave/exceptions_middleware.rb
71
- - lib/tidewave/file_tracker.rb
72
72
  - lib/tidewave/middleware.rb
73
+ - lib/tidewave/quiet_requests_middleware.rb
73
74
  - lib/tidewave/railtie.rb
74
75
  - lib/tidewave/tools/base.rb
75
- - lib/tidewave/tools/edit_project_file.rb
76
76
  - lib/tidewave/tools/execute_sql_query.rb
77
77
  - lib/tidewave/tools/get_docs.rb
78
78
  - lib/tidewave/tools/get_logs.rb
79
79
  - lib/tidewave/tools/get_models.rb
80
- - lib/tidewave/tools/get_package_location.rb
81
80
  - lib/tidewave/tools/get_source_location.rb
82
- - lib/tidewave/tools/list_project_files.rb
83
81
  - lib/tidewave/tools/project_eval.rb
84
- - lib/tidewave/tools/read_project_file.rb
85
- - lib/tidewave/tools/shell_eval.rb
86
- - lib/tidewave/tools/write_project_file.rb
87
82
  - lib/tidewave/version.rb
88
83
  homepage: https://tidewave.ai/
89
84
  licenses:
@@ -1,102 +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, include_ignored: false)
8
- args = [ "ls-files", "--cached", "--others" ]
9
- args << "--exclude-standard" unless include_ignored
10
- args << glob_pattern if glob_pattern
11
- `git #{args.join(" ")}`.split("\n")
12
- end
13
-
14
- def read_file(path, line_offset: 0, count: nil)
15
- full_path = file_full_path(path)
16
- # Explicitly read the mtime first to avoid race conditions
17
- mtime = File.mtime(full_path).to_i
18
- content = File.read(full_path)
19
-
20
- if line_offset > 0 || count
21
- lines = content.lines
22
- start_idx = [ line_offset, 0 ].max
23
- count = (count || lines.length)
24
- selected_lines = lines[start_idx, count]
25
- content = selected_lines ? selected_lines.join : ""
26
- end
27
-
28
- [ mtime, content ]
29
- end
30
-
31
- def write_file(path, content)
32
- validate_ruby_syntax!(content) if ruby_file?(path)
33
- full_path = file_full_path(path)
34
-
35
- # Create the directory if it doesn't exist
36
- dirname = File.dirname(full_path)
37
- FileUtils.mkdir_p(dirname)
38
-
39
- # Write and return the file contents
40
- File.write(full_path, content)
41
- content
42
- end
43
-
44
- def file_full_path(path)
45
- File.expand_path(path, Rails.root)
46
- end
47
-
48
- def validate_path_access!(path, validate_existence: true)
49
- raise ArgumentError, "File path must not contain '..'" if path.include?("..")
50
-
51
- # Ensure the path is within the project
52
- full_path = file_full_path(path)
53
-
54
- # Verify the file is within the project directory
55
- unless full_path.start_with?(Rails.root.to_s + File::SEPARATOR)
56
- raise ArgumentError, "File path must be within the project directory"
57
- end
58
-
59
- # Verify the file exists
60
- if validate_existence && !File.exist?(full_path)
61
- raise ArgumentError, "File not found: #{path}"
62
- end
63
-
64
- true
65
- end
66
-
67
- def validate_path_is_editable!(path, atime)
68
- validate_path_access!(path)
69
- validate_path_has_been_read_since_last_write!(path, atime)
70
-
71
- true
72
- end
73
-
74
- def validate_path_is_writable!(path, atime)
75
- validate_path_access!(path, validate_existence: false)
76
- validate_path_has_been_read_since_last_write!(path, atime)
77
-
78
- true
79
- end
80
-
81
- private
82
-
83
- def ruby_file?(path)
84
- [ ".rb", ".rake", ".gemspec" ].include?(File.extname(path)) ||
85
- [ "Gemfile" ].include?(File.basename(path))
86
- end
87
-
88
- def validate_ruby_syntax!(content)
89
- RubyVM::AbstractSyntaxTree.parse(content)
90
- rescue SyntaxError => e
91
- raise "Invalid Ruby syntax: #{e.message}"
92
- end
93
-
94
- def validate_path_has_been_read_since_last_write!(path, atime)
95
- if atime && File.mtime(file_full_path(path)).to_i > atime
96
- raise ArgumentError, "File has been modified since last read, please read the file again"
97
- end
98
-
99
- true
100
- end
101
- end
102
- 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
- _mtime, 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,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tidewave/file_tracker"
4
-
5
- class Tidewave::Tools::ListProjectFiles < Tidewave::Tools::Base
6
- tags :file_system_tool
7
-
8
- tool_name "list_project_files"
9
- description <<~DESC
10
- Returns a list of files in the project.
11
-
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)
25
- end
26
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tidewave/file_tracker"
4
-
5
- class Tidewave::Tools::ReadProjectFile < Tidewave::Tools::Base
6
- tags :file_system_tool
7
-
8
- tool_name "read_project_file"
9
- description <<~DESCRIPTION
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.
12
- DESCRIPTION
13
-
14
- arguments do
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.")
18
- end
19
-
20
- def call(path:, **keywords)
21
- Tidewave::FileTracker.validate_path_access!(path)
22
- _meta[:mtime], contents = Tidewave::FileTracker.read_file(path, **keywords)
23
- contents
24
- end
25
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
-
5
- class Tidewave::Tools::ShellEval < Tidewave::Tools::Base
6
- tags :file_system_tool
7
- class CommandFailedError < StandardError; end
8
-
9
- tool_name "shell_eval"
10
- description <<~DESCRIPTION
11
- Executes a shell command in the project root directory.
12
-
13
- The operating system is of flavor #{RUBY_PLATFORM}.
14
-
15
- Avoid using this tool for manipulating project files.
16
- Instead rely on the tools with the name matching `*_project_files`.
17
-
18
- Do not use this tool to evaluate Ruby code. Use `project_eval` instead.
19
- Do not use this tool for commands that run indefinitely,
20
- such as servers (like `bin/dev` or `npm run dev`),
21
- REPLs (`bin/rails console`) or file watchers.
22
-
23
- Only use this tool if other means are not available.
24
- DESCRIPTION
25
-
26
- arguments do
27
- required(:command).filled(:string).description("The shell command to execute. Avoid using this for file operations; use dedicated file system tools instead.")
28
- end
29
-
30
- def call(command:)
31
- stdout, status = Open3.capture2e(command)
32
- raise CommandFailedError, "Command failed with status #{status.exitstatus}:\n\n#{stdout}" unless status.exitstatus.zero?
33
-
34
- stdout.strip
35
- end
36
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tidewave/file_tracker"
4
-
5
- class Tidewave::Tools::WriteProjectFile < Tidewave::Tools::Base
6
- tags :file_system_tool
7
-
8
- tool_name "write_project_file"
9
- description <<~DESCRIPTION
10
- Writes a file to the file system. If the file already exists, it will be overwritten.
11
-
12
- Note that this tool will fail if the file wasn't previously read with the `read_project_file` tool.
13
- DESCRIPTION
14
-
15
- arguments do
16
- required(:path).filled(:string).description("The path to the file to write. It is relative to the project root.")
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.")
19
- end
20
-
21
- def call(path:, content:, atime: nil)
22
- Tidewave::FileTracker.validate_path_is_writable!(path, atime)
23
- Tidewave::FileTracker.write_file(path, content)
24
- _meta[:mtime] = Time.now.to_i
25
-
26
- "OK"
27
- end
28
- end