cogbox 0.190.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +190 -0
  7. data/README.md +184 -0
  8. data/Rakefile +12 -0
  9. data/lib/cogbox/code_interpreter.rb +360 -0
  10. data/lib/cogbox/cogbox.rb +337 -0
  11. data/lib/cogbox/common/charts.rb +125 -0
  12. data/lib/cogbox/common/code_interpreter.rb +57 -0
  13. data/lib/cogbox/common/code_language.rb +15 -0
  14. data/lib/cogbox/common/cogbox.rb +231 -0
  15. data/lib/cogbox/common/file_system.rb +27 -0
  16. data/lib/cogbox/common/git.rb +20 -0
  17. data/lib/cogbox/common/image.rb +501 -0
  18. data/lib/cogbox/common/process.rb +150 -0
  19. data/lib/cogbox/common/pty.rb +310 -0
  20. data/lib/cogbox/common/resources.rb +40 -0
  21. data/lib/cogbox/common/response.rb +84 -0
  22. data/lib/cogbox/common/snapshot.rb +125 -0
  23. data/lib/cogbox/computer_use.rb +920 -0
  24. data/lib/cogbox/config.rb +110 -0
  25. data/lib/cogbox/file_system.rb +452 -0
  26. data/lib/cogbox/file_transfer.rb +384 -0
  27. data/lib/cogbox/git.rb +335 -0
  28. data/lib/cogbox/lsp_server.rb +140 -0
  29. data/lib/cogbox/object_storage.rb +173 -0
  30. data/lib/cogbox/otel.rb +184 -0
  31. data/lib/cogbox/process.rb +551 -0
  32. data/lib/cogbox/sandbox.rb +752 -0
  33. data/lib/cogbox/sdk/version.rb +11 -0
  34. data/lib/cogbox/sdk.rb +57 -0
  35. data/lib/cogbox/snapshot_service.rb +239 -0
  36. data/lib/cogbox/util.rb +81 -0
  37. data/lib/cogbox/volume.rb +47 -0
  38. data/lib/cogbox/volume_service.rb +62 -0
  39. data/lib/cogbox.rb +11 -0
  40. data/project.json +100 -0
  41. data/scripts/generate-docs.rb +403 -0
  42. data/sig/cogbox/sdk.rbs +6 -0
  43. metadata +240 -0
@@ -0,0 +1,110 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # Copyright Cognifyi
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require 'dotenv'
8
+
9
+ module Cogbox
10
+ class Config
11
+ API_URL = 'https://cogbox.pazity.com/api'
12
+
13
+ # API key for authentication with the Cogbox API
14
+ #
15
+ # @return [String, nil] Cogbox API key
16
+ attr_accessor :api_key
17
+
18
+ # JWT token for authentication with the Cogbox API
19
+ #
20
+ # @return [String, nil] Cogbox JWT token
21
+ attr_accessor :jwt_token
22
+
23
+ # URL of the Cogbox API
24
+ #
25
+ # @return [String, nil] Cogbox API URL
26
+ attr_accessor :api_url
27
+
28
+ # Organization ID for authentication with the Cogbox API
29
+ #
30
+ # @return [String, nil] Cogbox API URL
31
+ attr_accessor :organization_id
32
+
33
+ # Target environment for sandboxes
34
+ #
35
+ # @return [String, nil] Cogbox target
36
+ attr_accessor :target
37
+
38
+ # Enable OpenTelemetry tracing for SDK operations.
39
+ #
40
+ # @return [Boolean, nil]
41
+ attr_accessor :otel_enabled
42
+
43
+ # Experimental configuration options
44
+ #
45
+ # @return [Hash, nil] Experimental configuration hash
46
+ attr_accessor :_experimental
47
+
48
+ # Initializes a new Cogbox::Config object.
49
+ #
50
+ # @param api_key [String, nil] Cogbox API key. Defaults to ENV['COGBOX_API_KEY'].
51
+ # @param jwt_token [String, nil] Cogbox JWT token. Defaults to ENV['COGBOX_JWT_TOKEN'].
52
+ # @param api_url [String, nil] Cogbox API URL. Defaults to ENV['COGBOX_API_URL'] or Cogbox::Config::API_URL.
53
+ # @param organization_id [String, nil] Cogbox organization ID. Defaults to ENV['COGBOX_ORGANIZATION_ID'].
54
+ # @param target [String, nil] Cogbox target. Defaults to ENV['COGBOX_TARGET'].
55
+ # @param otel_enabled [Boolean, nil] Enable OpenTelemetry tracing for SDK operations.
56
+ # @param _experimental [Hash, nil] Experimental configuration options.
57
+ def initialize( # rubocop:disable Metrics/ParameterLists
58
+ api_key: nil,
59
+ jwt_token: nil,
60
+ api_url: nil,
61
+ organization_id: nil,
62
+ target: nil,
63
+ otel_enabled: nil,
64
+ _experimental: nil
65
+ )
66
+ @env_reader = cogbox_env_reader
67
+
68
+ @api_key = api_key || @env_reader.call('COGBOX_API_KEY')
69
+ @jwt_token = jwt_token || @env_reader.call('COGBOX_JWT_TOKEN')
70
+ @api_url = api_url || @env_reader.call('COGBOX_API_URL') || API_URL
71
+ @target = target || @env_reader.call('COGBOX_TARGET')
72
+ @organization_id = organization_id || @env_reader.call('COGBOX_ORGANIZATION_ID')
73
+ @otel_enabled = otel_enabled
74
+ @_experimental = _experimental
75
+ end
76
+
77
+ # Reads a COGBOX_-prefixed environment variable using the same precedence
78
+ # as the Config initializer: runtime ENV first, then .env.local, then .env.
79
+ # Only names starting with COGBOX_ are accepted.
80
+ #
81
+ # @param name [String] The environment variable name. Must start with COGBOX_.
82
+ # @return [String, nil] The value of the environment variable, or nil if not set.
83
+ # @raise [ArgumentError] If name does not start with COGBOX_.
84
+ def read_env(name)
85
+ @env_reader.call(name)
86
+ end
87
+
88
+ private
89
+
90
+ # Returns a lambda that looks up COGBOX_-prefixed env vars without writing to ENV.
91
+ # Files are parsed once; lookups check runtime env first, then .env.local, then .env.
92
+ def cogbox_env_reader
93
+ file_vars = {}
94
+ env_file = File.join(Dir.pwd, '.env')
95
+ file_vars.merge!(cogbox_filter(Dotenv.parse(env_file))) if File.exist?(env_file)
96
+ env_local_file = File.join(Dir.pwd, '.env.local')
97
+ file_vars.merge!(cogbox_filter(Dotenv.parse(env_local_file))) if File.exist?(env_local_file)
98
+
99
+ lambda do |name|
100
+ raise ArgumentError, "Variable must start with 'COGBOX_', got '#{name}'" unless name.start_with?('COGBOX_')
101
+
102
+ ENV.key?(name) ? ENV[name] : file_vars[name]
103
+ end
104
+ end
105
+
106
+ def cogbox_filter(env_hash)
107
+ env_hash.select { |k, _| k.start_with?('COGBOX_') }
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,452 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # Copyright Cognifyi
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require 'tempfile'
8
+ require 'fileutils'
9
+ require_relative 'file_transfer'
10
+
11
+ module Cogbox
12
+ class FileSystem # rubocop:disable Metrics/ClassLength
13
+ include Instrumentation
14
+
15
+ # @return [String] The Sandbox ID
16
+ attr_reader :sandbox_id
17
+
18
+ # @return [CogboxToolboxApiClient::FileSystemApi] API client for Sandbox operations
19
+ attr_reader :toolbox_api
20
+
21
+ # Initializes a new FileSystem instance.
22
+ #
23
+ # @param sandbox_id [String] The Sandbox ID
24
+ # @param toolbox_api [CogboxToolboxApiClient::FileSystemApi] API client for Sandbox operations
25
+ # @param otel_state [Cogbox::OtelState, nil]
26
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
27
+ @sandbox_id = sandbox_id
28
+ @toolbox_api = toolbox_api
29
+ @otel_state = otel_state
30
+ end
31
+
32
+ # Creates a new directory in the Sandbox at the specified path with the given
33
+ # permissions.
34
+ #
35
+ # @param path [String] Path where the folder should be created. Relative paths are resolved based
36
+ # on the sandbox working directory.
37
+ # @param mode [String] Folder permissions in octal format (e.g., "755" for rwxr-xr-x).
38
+ # @return [void]
39
+ # @raise [Cogbox::Sdk::Error] If the operation fails
40
+ #
41
+ # @example
42
+ # # Create a directory with standard permissions
43
+ # sandbox.fs.create_folder("workspace/data", "755")
44
+ #
45
+ # # Create a private directory
46
+ # sandbox.fs.create_folder("workspace/secrets", "700")
47
+ def create_folder(path, mode)
48
+ Sdk.logger.debug("Creating folder #{path} with mode #{mode}")
49
+ toolbox_api.create_folder(path, mode)
50
+ rescue StandardError => e
51
+ raise Sdk::Error, "Failed to create folder: #{e.message}"
52
+ end
53
+
54
+ # Deletes a file from the Sandbox.
55
+ #
56
+ # @param path [String] Path to the file to delete. Relative paths are resolved based on the sandbox working directory.
57
+ # @param recursive [Boolean] If the file is a directory, this must be true to delete it.
58
+ # @return [void]
59
+ # @raise [Cogbox::Sdk::Error] If the operation fails
60
+ #
61
+ # @example
62
+ # # Delete a file
63
+ # sandbox.fs.delete_file("workspace/data/old_file.txt")
64
+ #
65
+ # # Delete a directory recursively
66
+ # sandbox.fs.delete_file("workspace/old_dir", recursive: true)
67
+ def delete_file(path, recursive: false)
68
+ toolbox_api.delete_file(path, { recursive: })
69
+ rescue StandardError => e
70
+ raise Sdk::Error, "Failed to delete file: #{e.message}"
71
+ end
72
+
73
+ # Gets detailed information about a file or directory, including its
74
+ # size, permissions, and timestamps.
75
+ #
76
+ # @param path [String] Path to the file or directory. Relative paths are resolved based
77
+ # on the sandbox working directory.
78
+ # @return [CogboxApiClient::FileInfo] Detailed file information
79
+ # @raise [Cogbox::Sdk::Error] If the operation fails
80
+ #
81
+ # @example
82
+ # # Get file metadata
83
+ # info = sandbox.fs.get_file_info("workspace/data/file.txt")
84
+ # puts "Size: #{info.size} bytes"
85
+ # puts "Modified: #{info.mod_time}"
86
+ # puts "Mode: #{info.mode}"
87
+ #
88
+ # # Check if path is a directory
89
+ # info = sandbox.fs.get_file_info("workspace/data")
90
+ # puts "Path is a directory" if info.is_dir
91
+ def get_file_info(path)
92
+ toolbox_api.get_file_info(path)
93
+ rescue StandardError => e
94
+ raise Sdk::Error, "Failed to get file info: #{e.message}"
95
+ end
96
+
97
+ # Lists files and directories in a given path and returns their information, similar to the ls -l command.
98
+ #
99
+ # @param path [String] Path to the directory to list contents from. Relative paths are resolved
100
+ # based on the sandbox working directory.
101
+ # @return [Array<CogboxApiClient::FileInfo>] List of file and directory information
102
+ # @raise [Cogbox::Sdk::Error] If the operation fails
103
+ #
104
+ # @example
105
+ # # List directory contents
106
+ # files = sandbox.fs.list_files("workspace/data")
107
+ #
108
+ # # Print files and their sizes
109
+ # files.each do |file|
110
+ # puts "#{file.name}: #{file.size} bytes" unless file.is_dir
111
+ # end
112
+ #
113
+ # # List only directories
114
+ # dirs = files.select(&:is_dir)
115
+ # puts "Subdirectories: #{dirs.map(&:name).join(', ')}"
116
+ def list_files(path)
117
+ toolbox_api.list_files({ path: })
118
+ rescue StandardError => e
119
+ raise Sdk::Error, "Failed to list files: #{e.message}"
120
+ end
121
+
122
+ # Downloads a file from the Sandbox. Returns the file contents as a string.
123
+ # This method is useful when you want to load the file into memory without saving it to disk.
124
+ # It can only be used for smaller files.
125
+ #
126
+ # @param remote_path [String] Path to the file in the Sandbox. Relative paths are resolved based
127
+ # on the sandbox working directory.
128
+ # @param local_path [String, nil] Optional path to save the file locally. If provided, the file will be saved to disk.
129
+ # @return [File, nil] The file if local_path is nil, otherwise nil
130
+ # @raise [Cogbox::Sdk::Error] If the operation fails
131
+ #
132
+ # @example
133
+ # # Download and get file content
134
+ # content = sandbox.fs.download_file("workspace/data/file.txt")
135
+ # puts content
136
+ #
137
+ # # Download and save a file locally
138
+ # sandbox.fs.download_file("workspace/data/file.txt", "local_copy.txt")
139
+ # size_mb = File.size("local_copy.txt") / 1024.0 / 1024.0
140
+ # puts "Size of the downloaded file: #{size_mb} MB"
141
+ def download_file(remote_path, local_path = nil) # rubocop:disable Metrics/MethodLength
142
+ file = toolbox_api.download_file(remote_path)
143
+
144
+ if local_path
145
+
146
+ parent_dir = File.dirname(local_path)
147
+ FileUtils.mkdir_p(parent_dir) unless parent_dir == '.'
148
+
149
+ File.binwrite(local_path, file.open.read)
150
+ nil
151
+ else
152
+ file
153
+ end
154
+ rescue StandardError => e
155
+ raise Sdk::Error, "Failed to download file: #{e.message}"
156
+ end
157
+
158
+ # Downloads a single file from the Sandbox as a stream without buffering the entire
159
+ # file into memory. Yields file content in chunks to the given block, or returns an
160
+ # Enumerator if no block is given.
161
+ #
162
+ # @param remote_path [String] Path to the file in the Sandbox. Relative paths are resolved
163
+ # based on the sandbox working directory.
164
+ # @param timeout [Integer] Timeout for the download operation in seconds. 0 means no timeout.
165
+ # Default is 30 minutes.
166
+ # @param on_progress [Proc, nil] Optional callback invoked with a Cogbox::DownloadProgress
167
+ # struct containing bytes_received (Integer) and total_bytes (Integer or nil).
168
+ # @param cancel_event [#set?, nil] Optional cancellation token (anything responding to +set?+;
169
+ # the standard library's +Concurrent::Event+ or a small ad-hoc object both work). When set
170
+ # during streaming, the next chunk raises Cogbox::Sdk::Error and the underlying HTTP
171
+ # connection is torn down.
172
+ # @yield [chunk] Yields each chunk of file content as it arrives
173
+ # @yieldparam chunk [String] A binary string chunk of file content
174
+ # @return [Enumerator, nil] An Enumerator yielding chunks if no block given, nil otherwise
175
+ # @raise [Cogbox::Sdk::Error] If the file does not exist, the operation fails, or
176
+ # +cancel_event+ is set during streaming
177
+ #
178
+ # @example Stream to a local file without loading into memory
179
+ # File.open("local_copy.bin", "wb") do |f|
180
+ # sandbox.fs.download_file_stream("workspace/large-file.bin") { |chunk| f.write(chunk) }
181
+ # end
182
+ #
183
+ # @example Collect chunks with an Enumerator
184
+ # content = sandbox.fs.download_file_stream("workspace/data.json").reduce(:+)
185
+ # puts content
186
+ def download_file_stream(remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil, &)
187
+ return enum_for(__method__, remote_path, timeout:, on_progress:, cancel_event:) unless block_given?
188
+
189
+ FileTransfer.stream_download(api_client: toolbox_api.api_client, remote_path: remote_path,
190
+ timeout: timeout, on_progress: on_progress,
191
+ cancel_event: cancel_event, &)
192
+ nil
193
+ rescue StandardError => e
194
+ raise Sdk::Error, "Failed to download file: #{e.message}"
195
+ end
196
+
197
+ # Uploads a file to the specified path in the Sandbox. If a file already exists at
198
+ # the destination path, it will be overwritten.
199
+ #
200
+ # @param source [String, IO] File contents as a string/bytes or a local file path or IO object.
201
+ # @param remote_path [String] Path to the destination file. Relative paths are resolved based on
202
+ # the sandbox working directory.
203
+ # @return [void]
204
+ # @raise [Cogbox::Sdk::Error] If the operation fails
205
+ #
206
+ # @example
207
+ # # Upload a text file from string content
208
+ # content = "Hello, World!"
209
+ # sandbox.fs.upload_file(content, "tmp/hello.txt")
210
+ #
211
+ # # Upload a local file
212
+ # sandbox.fs.upload_file("local_file.txt", "tmp/file.txt")
213
+ #
214
+ # # Upload binary data
215
+ # data = { key: "value" }.to_json
216
+ # sandbox.fs.upload_file(data, "tmp/config.json")
217
+ def upload_file(source, remote_path)
218
+ if source.is_a?(String) && File.exist?(source)
219
+ # Source is a file path
220
+ File.open(source, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
221
+ elsif source.respond_to?(:read)
222
+ # Source is an IO object
223
+ toolbox_api.upload_file(remote_path, source)
224
+ else
225
+ # Tempfile.create yields a ::File (works with Typhoeus) but deletes
226
+ # on block exit — too early if curl reads asynchronously. Write via
227
+ # Tempfile, close it, then reopen as ::File for the upload.
228
+ tmp = Tempfile.new('cogbox_upload')
229
+ begin
230
+ tmp.binmode
231
+ tmp.write(source.to_s.b)
232
+ tmp.close
233
+ File.open(tmp.path, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
234
+ ensure
235
+ tmp.unlink
236
+ end
237
+ end
238
+ rescue StandardError => e
239
+ raise Sdk::Error, "Failed to upload file: #{e.message}"
240
+ end
241
+
242
+ # Streams +source+ to the Sandbox without buffering its contents in memory, with
243
+ # optional progress reporting.
244
+ #
245
+ # @param source [String, IO] A local file path or any IO-like object responding to
246
+ # +read(n)+. Strings that don't reference an existing file are uploaded as their
247
+ # raw bytes (still streamed, just from memory).
248
+ # @param remote_path [String] Destination path in the Sandbox.
249
+ # @param timeout [Integer] Timeout in seconds. 0 means no timeout. Default 30 minutes.
250
+ # @param on_progress [Proc, nil] Optional callback invoked with a
251
+ # +Cogbox::UploadProgress+ struct as libcurl reports bytes actually uploaded.
252
+ # @param cancel_event [#set?, nil] Optional cancellation token. When set while
253
+ # staging a non-file source or during the libcurl upload, the operation raises
254
+ # Cogbox::Sdk::Error and the in-progress upload is aborted (no destination file
255
+ # is left on the sandbox thanks to the daemon's atomic-rename behaviour).
256
+ # @return [void]
257
+ # @raise [Cogbox::Sdk::Error] If the operation fails or +cancel_event+ is set.
258
+ #
259
+ # @example
260
+ # File.open("large.bin", "rb") do |f|
261
+ # sandbox.fs.upload_file_stream(f, "tmp/large.bin",
262
+ # on_progress: ->(p) { puts "#{p.bytes_sent} bytes sent" })
263
+ # end
264
+ def upload_file_stream(source, remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil)
265
+ FileTransfer.stream_upload(api_client: toolbox_api.api_client, remote_path: remote_path,
266
+ source: source, timeout: timeout, on_progress: on_progress,
267
+ cancel_event: cancel_event)
268
+ rescue StandardError => e
269
+ raise Sdk::Error, "Failed to upload file: #{e.message}"
270
+ end
271
+
272
+ # Uploads multiple files to the Sandbox. If files already exist at the destination paths,
273
+ # they will be overwritten.
274
+ #
275
+ # @param files [Array<FileUpload>] List of files to upload.
276
+ # @return [void]
277
+ # @raise [Cogbox::Sdk::Error] If the operation fails
278
+ #
279
+ # @example
280
+ # # Upload multiple files
281
+ # files = [
282
+ # FileUpload.new("Content of file 1", "/tmp/file1.txt"),
283
+ # FileUpload.new("workspace/data/file2.txt", "/tmp/file2.txt"),
284
+ # FileUpload.new('{"key": "value"}', "/tmp/config.json")
285
+ # ]
286
+ # sandbox.fs.upload_files(files)
287
+ def upload_files(files)
288
+ files.each { |file_upload| upload_file(file_upload.source, file_upload.destination) }
289
+ rescue StandardError => e
290
+ raise Sdk::Error, "Failed to upload files: #{e.message}"
291
+ end
292
+
293
+ # Searches for files containing a pattern, similar to the grep command.
294
+ #
295
+ # @param path [String] Path to the file or directory to search. If the path is a directory,
296
+ # the search will be performed recursively. Relative paths are resolved based
297
+ # on the sandbox working directory.
298
+ # @param pattern [String] Search pattern to match against file contents.
299
+ # @return [Array<CogboxApiClient::Match>] List of matches found in files
300
+ # @raise [Cogbox::Sdk::Error] If the operation fails
301
+ #
302
+ # @example
303
+ # # Search for TODOs in Ruby files
304
+ # matches = sandbox.fs.find_files("workspace/src", "TODO:")
305
+ # matches.each do |match|
306
+ # puts "#{match.file}:#{match.line}: #{match.content.strip}"
307
+ # end
308
+ def find_files(path, pattern)
309
+ toolbox_api.find_in_files(path, pattern)
310
+ rescue StandardError => e
311
+ raise Sdk::Error, "Failed to find files: #{e.message}"
312
+ end
313
+
314
+ # Searches for files and directories whose names match the specified pattern.
315
+ # The pattern can be a simple string or a glob pattern.
316
+ #
317
+ # @param path [String] Path to the root directory to start search from. Relative paths are resolved
318
+ # based on the sandbox working directory.
319
+ # @param pattern [String] Pattern to match against file names. Supports glob
320
+ # patterns (e.g., "*.rb" for Ruby files).
321
+ # @return [CogboxApiClient::SearchFilesResponse]
322
+ # @raise [Cogbox::Sdk::Error] If the operation fails
323
+ #
324
+ # @example
325
+ # # Find all Ruby files
326
+ # result = sandbox.fs.search_files("workspace", "*.rb")
327
+ # result.files.each { |file| puts file }
328
+ #
329
+ # # Find files with specific prefix
330
+ # result = sandbox.fs.search_files("workspace/data", "test_*")
331
+ # puts "Found #{result.files.length} test files"
332
+ def search_files(path, pattern)
333
+ toolbox_api.search_files(path, pattern)
334
+ rescue StandardError => e
335
+ raise Sdk::Error, "Failed to search files: #{e.message}"
336
+ end
337
+
338
+ # Moves or renames a file or directory. The parent directory of the destination must exist.
339
+ #
340
+ # @param source [String] Path to the source file or directory. Relative paths are resolved
341
+ # based on the sandbox working directory.
342
+ # @param destination [String] Path to the destination. Relative paths are resolved based on
343
+ # the sandbox working directory.
344
+ # @return [void]
345
+ # @raise [Cogbox::Sdk::Error] If the operation fails
346
+ #
347
+ # @example
348
+ # # Rename a file
349
+ # sandbox.fs.move_files(
350
+ # "workspace/data/old_name.txt",
351
+ # "workspace/data/new_name.txt"
352
+ # )
353
+ #
354
+ # # Move a file to a different directory
355
+ # sandbox.fs.move_files(
356
+ # "workspace/data/file.txt",
357
+ # "workspace/archive/file.txt"
358
+ # )
359
+ #
360
+ # # Move a directory
361
+ # sandbox.fs.move_files(
362
+ # "workspace/old_dir",
363
+ # "workspace/new_dir"
364
+ # )
365
+ def move_files(source, destination)
366
+ toolbox_api.move_file(source, destination)
367
+ rescue StandardError => e
368
+ raise Sdk::Error, "Failed to move files: #{e.message}"
369
+ end
370
+
371
+ # Performs search and replace operations across multiple files.
372
+ #
373
+ # @param files [Array<String>] List of file paths to perform replacements in. Relative paths are
374
+ # resolved based on the sandbox working directory.
375
+ # @param pattern [String] Pattern to search for.
376
+ # @param new_value [String] Text to replace matches with.
377
+ # @return [Array<CogboxApiClient::ReplaceResult>] List of results indicating replacements made in each file
378
+ # @raise [Cogbox::Sdk::Error] If the operation fails
379
+ #
380
+ # @example
381
+ # # Replace in specific files
382
+ # results = sandbox.fs.replace_in_files(
383
+ # files: ["workspace/src/file1.rb", "workspace/src/file2.rb"],
384
+ # pattern: "old_function",
385
+ # new_value: "new_function"
386
+ # )
387
+ #
388
+ # # Print results
389
+ # results.each do |result|
390
+ # if result.success
391
+ # puts "#{result.file}: #{result.success}"
392
+ # else
393
+ # puts "#{result.file}: #{result.error}"
394
+ # end
395
+ # end
396
+ def replace_in_files(files:, pattern:, new_value:)
397
+ replace_request = CogboxApiClient::ReplaceRequest.new(
398
+ files: files,
399
+ pattern: pattern,
400
+ new_value: new_value
401
+ )
402
+ toolbox_api.replace_in_files(replace_request)
403
+ rescue StandardError => e
404
+ raise Sdk::Error, "Failed to replace in files: #{e.message}"
405
+ end
406
+
407
+ # Sets permissions and ownership for a file or directory. Any of the parameters can be nil
408
+ # to leave that attribute unchanged.
409
+ #
410
+ # @param path [String] Path to the file or directory. Relative paths are resolved based on
411
+ # the sandbox working directory.
412
+ # @param mode [String, nil] File mode/permissions in octal format (e.g., "644" for rw-r--r--).
413
+ # @param owner [String, nil] User owner of the file.
414
+ # @param group [String, nil] Group owner of the file.
415
+ # @return [void]
416
+ # @raise [Cogbox::Sdk::Error] If the operation fails
417
+ #
418
+ # @example
419
+ # # Make a file executable
420
+ # sandbox.fs.set_file_permissions(
421
+ # path: "workspace/scripts/run.sh",
422
+ # mode: "755" # rwxr-xr-x
423
+ # )
424
+ #
425
+ # # Change file owner
426
+ # sandbox.fs.set_file_permissions(
427
+ # path: "workspace/data/file.txt",
428
+ # owner: "cogbox",
429
+ # group: "cogbox"
430
+ # )
431
+ def set_file_permissions(path:, mode: nil, owner: nil, group: nil)
432
+ opts = {}
433
+ opts[:mode] = mode if mode
434
+ opts[:owner] = owner if owner
435
+ opts[:group] = group if group
436
+
437
+ toolbox_api.set_file_permissions(path, opts)
438
+ rescue StandardError => e
439
+ raise Sdk::Error, "Failed to set file permissions: #{e.message}"
440
+ end
441
+
442
+ instrument :create_folder, :delete_file, :get_file_info, :list_files, :download_file,
443
+ :download_file_stream, :upload_file, :upload_files, :find_files,
444
+ :search_files, :move_files, :replace_in_files, :set_file_permissions,
445
+ component: 'FileSystem'
446
+
447
+ private
448
+
449
+ # @return [Cogbox::OtelState, nil]
450
+ attr_reader :otel_state
451
+ end
452
+ end