nightona 0.191.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.
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/nightona/code_interpreter.rb +359 -0
  10. data/lib/nightona/common/charts.rb +124 -0
  11. data/lib/nightona/common/code_interpreter.rb +56 -0
  12. data/lib/nightona/common/code_language.rb +14 -0
  13. data/lib/nightona/common/file_system.rb +26 -0
  14. data/lib/nightona/common/git.rb +19 -0
  15. data/lib/nightona/common/image.rb +500 -0
  16. data/lib/nightona/common/nightona.rb +230 -0
  17. data/lib/nightona/common/process.rb +149 -0
  18. data/lib/nightona/common/pty.rb +309 -0
  19. data/lib/nightona/common/resources.rb +39 -0
  20. data/lib/nightona/common/response.rb +83 -0
  21. data/lib/nightona/common/snapshot.rb +124 -0
  22. data/lib/nightona/computer_use.rb +919 -0
  23. data/lib/nightona/config.rb +116 -0
  24. data/lib/nightona/file_system.rb +451 -0
  25. data/lib/nightona/file_transfer.rb +383 -0
  26. data/lib/nightona/git.rb +334 -0
  27. data/lib/nightona/lsp_server.rb +139 -0
  28. data/lib/nightona/nightona.rb +336 -0
  29. data/lib/nightona/object_storage.rb +172 -0
  30. data/lib/nightona/otel.rb +183 -0
  31. data/lib/nightona/process.rb +550 -0
  32. data/lib/nightona/sandbox.rb +751 -0
  33. data/lib/nightona/sdk/version.rb +10 -0
  34. data/lib/nightona/sdk.rb +56 -0
  35. data/lib/nightona/snapshot_service.rb +238 -0
  36. data/lib/nightona/util.rb +80 -0
  37. data/lib/nightona/volume.rb +46 -0
  38. data/lib/nightona/volume_service.rb +61 -0
  39. data/lib/nightona.rb +10 -0
  40. data/project.json +100 -0
  41. data/scripts/generate-docs.rb +402 -0
  42. data/sig/nightona/sdk.rbs +6 -0
  43. metadata +242 -0
@@ -0,0 +1,919 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Nightona
7
+ class ComputerUse
8
+ class Mouse
9
+ include Instrumentation
10
+
11
+ # @return [String] The ID of the sandbox
12
+ attr_reader :sandbox_id
13
+
14
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
15
+ attr_reader :toolbox_api
16
+
17
+ # @param sandbox_id [String] The ID of the sandbox
18
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
19
+ # @param otel_state [Nightona::OtelState, nil]
20
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
21
+ @sandbox_id = sandbox_id
22
+ @toolbox_api = toolbox_api
23
+ @otel_state = otel_state
24
+ end
25
+
26
+ # Gets the current mouse cursor position.
27
+ #
28
+ # @return [NightonaToolboxApiClient::MousePosition] Current mouse position with x and y coordinates
29
+ # @raise [Nightona::Sdk::Error] If the operation fails
30
+ #
31
+ # @example
32
+ # position = sandbox.computer_use.mouse.get_position
33
+ # puts "Mouse is at: #{position.x}, #{position.y}"
34
+ def position
35
+ toolbox_api.get_mouse_position
36
+ rescue StandardError => e
37
+ raise Sdk::Error, "Failed to get mouse position: #{e.message}"
38
+ end
39
+
40
+ # Moves the mouse cursor to the specified coordinates.
41
+ #
42
+ # @param x [Integer] The x coordinate to move to
43
+ # @param y [Integer] The y coordinate to move to
44
+ # @return [NightonaToolboxApiClient::MouseMoveResponse] Move operation result
45
+ # @raise [Nightona::Sdk::Error] If the operation fails
46
+ #
47
+ # @example
48
+ # result = sandbox.computer_use.mouse.move(x: 100, y: 200)
49
+ # puts "Mouse moved to: #{result.x}, #{result.y}"
50
+ def move(x:, y:)
51
+ request = NightonaToolboxApiClient::MouseMoveRequest.new(x:, y:)
52
+ toolbox_api.move_mouse(request)
53
+ rescue StandardError => e
54
+ raise Sdk::Error, "Failed to move mouse: #{e.message}"
55
+ end
56
+
57
+ # Clicks the mouse at the specified coordinates.
58
+ #
59
+ # @param x [Integer] The x coordinate to click at
60
+ # @param y [Integer] The y coordinate to click at
61
+ # @param button [String] The mouse button to click ('left', 'right', 'middle'). Defaults to 'left'
62
+ # @param double [Boolean] Whether to perform a double-click. Defaults to false
63
+ # @return [NightonaToolboxApiClient::MouseClickResponse] Click operation result
64
+ # @raise [Nightona::Sdk::Error] If the operation fails
65
+ #
66
+ # @example
67
+ # # Single left click
68
+ # result = sandbox.computer_use.mouse.click(x: 100, y: 200)
69
+ #
70
+ # # Double click
71
+ # double_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'left', double: true)
72
+ #
73
+ # # Right click
74
+ # right_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'right')
75
+ def click(x:, y:, button: 'left', double: false)
76
+ request = NightonaToolboxApiClient::MouseClickRequest.new(x:, y:, button:, double:)
77
+ toolbox_api.click(request)
78
+ rescue StandardError => e
79
+ raise Sdk::Error, "Failed to click mouse: #{e.message}"
80
+ end
81
+
82
+ # Drags the mouse from start coordinates to end coordinates.
83
+ #
84
+ # @param start_x [Integer] The starting x coordinate
85
+ # @param start_y [Integer] The starting y coordinate
86
+ # @param end_x [Integer] The ending x coordinate
87
+ # @param end_y [Integer] The ending y coordinate
88
+ # @param button [String] The mouse button to use for dragging. Defaults to 'left'
89
+ # @return [NightonaToolboxApiClient::MouseDragResponse] Drag operation result
90
+ # @raise [Nightona::Sdk::Error] If the operation fails
91
+ #
92
+ # @example
93
+ # result = sandbox.computer_use.mouse.drag(start_x: 50, start_y: 50, end_x: 150, end_y: 150)
94
+ # puts "Drag ended at #{result.x}, #{result.y}"
95
+ def drag(start_x:, start_y:, end_x:, end_y:, button: 'left')
96
+ request = NightonaToolboxApiClient::MouseDragRequest.new(start_x:, start_y:, end_x:, end_y:, button:)
97
+ toolbox_api.drag(request)
98
+ rescue StandardError => e
99
+ raise Sdk::Error, "Failed to drag mouse: #{e.message}"
100
+ end
101
+
102
+ # Scrolls the mouse wheel at the specified coordinates.
103
+ #
104
+ # @param x [Integer] The x coordinate to scroll at
105
+ # @param y [Integer] The y coordinate to scroll at
106
+ # @param direction [String] The direction to scroll ('up' or 'down')
107
+ # @param amount [Integer] The amount to scroll. Defaults to 1
108
+ # @return [Boolean] Whether the scroll operation was successful
109
+ # @raise [Nightona::Sdk::Error] If the operation fails
110
+ #
111
+ # @example
112
+ # # Scroll up
113
+ # scroll_up = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'up', amount: 3)
114
+ #
115
+ # # Scroll down
116
+ # scroll_down = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'down', amount: 5)
117
+ def scroll(x:, y:, direction:, amount: 1)
118
+ request = NightonaToolboxApiClient::MouseScrollRequest.new(x:, y:, direction:, amount:)
119
+ toolbox_api.scroll(request)
120
+ true
121
+ rescue StandardError => e
122
+ raise Sdk::Error, "Failed to scroll mouse: #{e.message}"
123
+ end
124
+
125
+ instrument :position, :move, :click, :drag, :scroll, component: 'Mouse'
126
+
127
+ private
128
+
129
+ # @return [Nightona::OtelState, nil]
130
+ attr_reader :otel_state
131
+ end
132
+
133
+ # Keyboard operations for computer use functionality.
134
+ class Keyboard
135
+ include Instrumentation
136
+
137
+ # @return [String] The ID of the sandbox
138
+ attr_reader :sandbox_id
139
+
140
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
141
+ attr_reader :toolbox_api
142
+
143
+ # @param sandbox_id [String] The ID of the sandbox
144
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
145
+ # @param otel_state [Nightona::OtelState, nil]
146
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
147
+ @sandbox_id = sandbox_id
148
+ @toolbox_api = toolbox_api
149
+ @otel_state = otel_state
150
+ end
151
+
152
+ # Types the specified text.
153
+ #
154
+ # @param text [String] The text to type
155
+ # @param delay [Integer, nil] Delay between characters in milliseconds
156
+ # @return [void]
157
+ # @raise [Nightona::Sdk::Error] If the operation fails
158
+ #
159
+ # @example
160
+ # sandbox.computer_use.keyboard.type("Hello, World!")
161
+ #
162
+ # # With delay between characters
163
+ # sandbox.computer_use.keyboard.type("Slow typing", delay: 100)
164
+ def type(text:, delay: nil)
165
+ request = NightonaToolboxApiClient::KeyboardTypeRequest.new(text:, delay:)
166
+ toolbox_api.type_text(request)
167
+ rescue StandardError => e
168
+ raise Sdk::Error, "Failed to type text: #{e.message}"
169
+ end
170
+
171
+ # Presses a key with optional modifiers.
172
+ #
173
+ # @param key [String] The key to press. Canonical names include 'enter', 'escape', 'tab', letters, digits, unshifted punctuation, function keys, and grammar-safe numpad names such as 'num_plus'. Named keys are case-insensitive, and common aliases such as 'Return' and 'Escape' are normalized.
174
+ # @param modifiers [Array<String>, nil] Canonical modifier names are 'ctrl', 'alt', 'shift', and 'cmd'. Common aliases such as 'control', 'option', 'meta', and 'win' are normalized.
175
+ # @return [void]
176
+ # @raise [Nightona::Sdk::Error] If the operation fails
177
+ #
178
+ # @example
179
+ # # Press Enter
180
+ # sandbox.computer_use.keyboard.press("enter")
181
+ #
182
+ # # Press Ctrl+C
183
+ # sandbox.computer_use.keyboard.press("c", modifiers: ["ctrl"])
184
+ #
185
+ # # Press Ctrl+Shift+T
186
+ # sandbox.computer_use.keyboard.press("t", modifiers: ["ctrl", "shift"])
187
+ def press(key:, modifiers: nil)
188
+ request = NightonaToolboxApiClient::KeyboardPressRequest.new(key:, modifiers: modifiers || [])
189
+ toolbox_api.press_key(request)
190
+ rescue StandardError => e
191
+ raise Sdk::Error, "Failed to press key: #{e.message}"
192
+ end
193
+
194
+ # Presses a hotkey combination.
195
+ #
196
+ # @param keys [String] A single atomic hotkey chord (e.g., 'ctrl+c', 'alt+tab', 'cmd+shift+t', 'ctrl + c', 'shift'). Uses the same normalized key contract as #press.
197
+ # @return [void]
198
+ # @raise [Nightona::Sdk::Error] If the operation fails
199
+ #
200
+ # @example
201
+ # # Copy
202
+ # sandbox.computer_use.keyboard.hotkey("ctrl+c")
203
+ #
204
+ # # Paste
205
+ # sandbox.computer_use.keyboard.hotkey("ctrl+v")
206
+ #
207
+ # # Alt+Tab
208
+ # sandbox.computer_use.keyboard.hotkey("alt+tab")
209
+ def hotkey(keys:)
210
+ request = NightonaToolboxApiClient::KeyboardHotkeyRequest.new(keys:)
211
+ toolbox_api.press_hotkey(request)
212
+ rescue StandardError => e
213
+ raise Sdk::Error, "Failed to press hotkey: #{e.message}"
214
+ end
215
+
216
+ instrument :type, :press, :hotkey, component: 'Keyboard'
217
+
218
+ private
219
+
220
+ # @return [Nightona::OtelState, nil]
221
+ attr_reader :otel_state
222
+ end
223
+
224
+ # Screenshot operations for computer use functionality.
225
+ class Screenshot
226
+ include Instrumentation
227
+
228
+ # @return [String] The ID of the sandbox
229
+ attr_reader :sandbox_id
230
+
231
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
232
+ attr_reader :toolbox_api
233
+
234
+ # @param sandbox_id [String] The ID of the sandbox
235
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
236
+ # @param otel_state [Nightona::OtelState, nil]
237
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
238
+ @sandbox_id = sandbox_id
239
+ @toolbox_api = toolbox_api
240
+ @otel_state = otel_state
241
+ end
242
+
243
+ # Takes a screenshot of the entire screen.
244
+ #
245
+ # @param show_cursor [Boolean] Whether to show the cursor in the screenshot. Defaults to false
246
+ # @return [NightonaApiClient::ScreenshotResponse] Screenshot data with base64 encoded image
247
+ # @raise [Nightona::Sdk::Error] If the operation fails
248
+ #
249
+ # @example
250
+ # screenshot = sandbox.computer_use.screenshot.take_full_screen
251
+ # puts "Screenshot size: #{screenshot.width}x#{screenshot.height}"
252
+ #
253
+ # # With cursor visible
254
+ # with_cursor = sandbox.computer_use.screenshot.take_full_screen(show_cursor: true)
255
+ def take_full_screen(show_cursor: false)
256
+ toolbox_api.take_screenshot(show_cursor:)
257
+ rescue StandardError => e
258
+ raise Sdk::Error, "Failed to take screenshot: #{e.message}"
259
+ end
260
+
261
+ # Takes a screenshot of a specific region.
262
+ #
263
+ # @param region [ScreenshotRegion] The region to capture
264
+ # @param show_cursor [Boolean] Whether to show the cursor in the screenshot. Defaults to false
265
+ # @return [NightonaApiClient::RegionScreenshotResponse] Screenshot data with base64 encoded image
266
+ # @raise [Nightona::Sdk::Error] If the operation fails
267
+ #
268
+ # @example
269
+ # region = ScreenshotRegion.new(x: 100, y: 100, width: 300, height: 200)
270
+ # screenshot = sandbox.computer_use.screenshot.take_region(region)
271
+ # puts "Captured region: #{screenshot.region.width}x#{screenshot.region.height}"
272
+ def take_region(region:, show_cursor: false)
273
+ toolbox_api.take_region_screenshot(region.height, region.width, region.y, region.x, show_cursor:)
274
+ rescue StandardError => e
275
+ raise Sdk::Error, "Failed to take region screenshot: #{e.message}"
276
+ end
277
+
278
+ # Takes a compressed screenshot of the entire screen.
279
+ #
280
+ # @param options [ScreenshotOptions, nil] Compression and display options
281
+ # @return [NightonaApiClient::CompressedScreenshotResponse] Compressed screenshot data
282
+ # @raise [Nightona::Sdk::Error] If the operation fails
283
+ #
284
+ # @example
285
+ # # Default compression
286
+ # screenshot = sandbox.computer_use.screenshot.take_compressed
287
+ #
288
+ # # High quality JPEG
289
+ # jpeg = sandbox.computer_use.screenshot.take_compressed(
290
+ # options: ScreenshotOptions.new(format: "jpeg", quality: 95, show_cursor: true)
291
+ # )
292
+ #
293
+ # # Scaled down PNG
294
+ # scaled = sandbox.computer_use.screenshot.take_compressed(
295
+ # options: ScreenshotOptions.new(format: "png", scale: 0.5)
296
+ # )
297
+ def take_compressed(options: nil)
298
+ options ||= ScreenshotOptions.new
299
+ toolbox_api.take_compressed_screenshot(
300
+ sandbox_id,
301
+ scale: options.scale,
302
+ quality: options.quality,
303
+ format: options.fmt,
304
+ show_cursor: options.show_cursor
305
+ )
306
+ rescue StandardError => e
307
+ raise Sdk::Error, "Failed to take compressed screenshot: #{e.message}"
308
+ end
309
+
310
+ # Takes a compressed screenshot of a specific region.
311
+ #
312
+ # @param region [ScreenshotRegion] The region to capture
313
+ # @param options [ScreenshotOptions, nil] Compression and display options
314
+ # @return [NightonaApiClient::CompressedScreenshotResponse] Compressed screenshot data
315
+ # @raise [Nightona::Sdk::Error] If the operation fails
316
+ #
317
+ # @example
318
+ # region = ScreenshotRegion.new(x: 0, y: 0, width: 800, height: 600)
319
+ # screenshot = sandbox.computer_use.screenshot.take_compressed_region(
320
+ # region,
321
+ # options: ScreenshotOptions.new(format: "webp", quality: 80, show_cursor: true)
322
+ # )
323
+ # puts "Compressed size: #{screenshot.size_bytes} bytes"
324
+ def take_compressed_region(region:, options: nil)
325
+ options ||= ScreenshotOptions.new
326
+ toolbox_api.take_compressed_region_screenshot(
327
+ sandbox_id,
328
+ region.height,
329
+ region.width,
330
+ region.y,
331
+ region.x,
332
+ scale: options.scale,
333
+ quality: options.quality,
334
+ format: options.fmt,
335
+ show_cursor: options.show_cursor
336
+ )
337
+ rescue StandardError => e
338
+ raise Sdk::Error, "Failed to take compressed region screenshot: #{e.message}"
339
+ end
340
+
341
+ instrument :take_full_screen, :take_region, :take_compressed, :take_compressed_region,
342
+ component: 'Screenshot'
343
+
344
+ private
345
+
346
+ # @return [Nightona::OtelState, nil]
347
+ attr_reader :otel_state
348
+ end
349
+
350
+ # Display operations for computer use functionality.
351
+ class Display
352
+ include Instrumentation
353
+
354
+ # @return [String] The ID of the sandbox
355
+ attr_reader :sandbox_id
356
+
357
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
358
+ attr_reader :toolbox_api
359
+
360
+ # @param sandbox_id [String] The ID of the sandbox
361
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
362
+ # @param otel_state [Nightona::OtelState, nil]
363
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
364
+ @sandbox_id = sandbox_id
365
+ @toolbox_api = toolbox_api
366
+ @otel_state = otel_state
367
+ end
368
+
369
+ # Gets information about the displays.
370
+ #
371
+ # @return [NightonaToolboxApiClient::DisplayInfoResponse] Display information including primary display and all available displays
372
+ # @raise [Nightona::Sdk::Error] If the operation fails
373
+ #
374
+ # @example
375
+ # info = sandbox.computer_use.display.get_info
376
+ # puts "Primary display: #{info.primary_display.width}x#{info.primary_display.height}"
377
+ # puts "Total displays: #{info.total_displays}"
378
+ # info.displays.each_with_index do |display, i|
379
+ # puts "Display #{i}: #{display.width}x#{display.height} at #{display.x},#{display.y}"
380
+ # end
381
+ def info
382
+ toolbox_api.get_display_info
383
+ rescue StandardError => e
384
+ raise Sdk::Error, "Failed to get display info: #{e.message}"
385
+ end
386
+
387
+ # Gets the list of open windows.
388
+ #
389
+ # @return [NightonaToolboxApiClient::WindowsResponse] List of open windows with their IDs and titles
390
+ # @raise [Nightona::Sdk::Error] If the operation fails
391
+ #
392
+ # @example
393
+ # windows = sandbox.computer_use.display.get_windows
394
+ # puts "Found #{windows.count} open windows:"
395
+ # windows.windows.each do |window|
396
+ # puts "- #{window.title} (ID: #{window.id})"
397
+ # end
398
+ def windows
399
+ toolbox_api.get_windows
400
+ rescue StandardError => e
401
+ raise Sdk::Error, "Failed to get windows: #{e.message}"
402
+ end
403
+
404
+ instrument :info, :windows, component: 'Display'
405
+
406
+ private
407
+
408
+ # @return [Nightona::OtelState, nil]
409
+ attr_reader :otel_state
410
+ end
411
+
412
+ # Accessibility operations for computer use functionality.
413
+ class Accessibility
414
+ include Instrumentation
415
+
416
+ # @return [String] The ID of the sandbox
417
+ attr_reader :sandbox_id
418
+
419
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
420
+ attr_reader :toolbox_api
421
+
422
+ # @param sandbox_id [String] The ID of the sandbox
423
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
424
+ # @param otel_state [Nightona::OtelState, nil]
425
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
426
+ @sandbox_id = sandbox_id
427
+ @toolbox_api = toolbox_api
428
+ @otel_state = otel_state
429
+ end
430
+
431
+ # Fetches the AT-SPI accessibility tree.
432
+ #
433
+ # @param scope [String, nil] Tree scope to inspect: "focused", "pid", or "all"
434
+ # @param pid [Integer, nil] Process ID when scope is "pid"
435
+ # @param max_depth [Integer, nil] Maximum depth to descend; 0 returns only the root
436
+ # @return [NightonaToolboxApiClient::AccessibilityTreeResponse] Accessibility tree response
437
+ # @raise [Nightona::Sdk::Error] If the operation fails
438
+ #
439
+ # @example
440
+ # tree = sandbox.computer_use.accessibility.get_tree(scope: "all", max_depth: 3)
441
+ # puts tree.root.name
442
+ def get_tree(scope: nil, pid: nil, max_depth: nil)
443
+ opts = {}
444
+ opts[:scope] = scope unless scope.nil?
445
+ opts[:pid] = pid unless pid.nil?
446
+ opts[:max_depth] = max_depth unless max_depth.nil?
447
+
448
+ toolbox_api.get_accessibility_tree(opts)
449
+ rescue StandardError => e
450
+ raise Sdk::Error, "Failed to get accessibility tree: #{e.message}"
451
+ end
452
+
453
+ # Finds AT-SPI accessibility nodes matching the provided filters.
454
+ #
455
+ # @param scope [String, nil] Search scope: "focused", "pid", or "all"
456
+ # @param pid [Integer, nil] Process ID when scope is "pid"
457
+ # @param role [String, nil] Accessibility role to match, such as "button"
458
+ # @param name [String, nil] Accessible name to match
459
+ # @param name_match [String, nil] Name match mode, such as "exact" or "substring"
460
+ # @param states [Array<String>, nil] Required accessibility states
461
+ # @param limit [Integer, nil] Maximum number of matches
462
+ # @return [NightonaToolboxApiClient::AccessibilityNodesResponse] Matching accessibility nodes
463
+ # @raise [Nightona::Sdk::Error] If the operation fails
464
+ #
465
+ # @example
466
+ # buttons = sandbox.computer_use.accessibility.find_nodes(
467
+ # scope: "all",
468
+ # role: "button",
469
+ # name: "Submit",
470
+ # name_match: "substring"
471
+ # )
472
+ # puts buttons.matches.length
473
+ def find_nodes(scope: nil, pid: nil, role: nil, name: nil, name_match: nil, states: nil, limit: nil)
474
+ attrs = {}
475
+ attrs[:scope] = scope unless scope.nil?
476
+ attrs[:pid] = pid unless pid.nil?
477
+ attrs[:role] = role unless role.nil?
478
+ attrs[:name] = name unless name.nil?
479
+ attrs[:name_match] = name_match unless name_match.nil?
480
+ attrs[:states] = states unless states.nil?
481
+ attrs[:limit] = limit unless limit.nil?
482
+
483
+ request = NightonaToolboxApiClient::FindAccessibilityNodesRequest.new(attrs)
484
+ toolbox_api.find_accessibility_nodes(request)
485
+ rescue StandardError => e
486
+ raise Sdk::Error, "Failed to find accessibility nodes: #{e.message}"
487
+ end
488
+
489
+ # Focuses an AT-SPI accessibility node.
490
+ #
491
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
492
+ # @raise [Nightona::Sdk::Error] If the operation fails
493
+ #
494
+ # @example
495
+ # sandbox.computer_use.accessibility.focus_node(id: node.id)
496
+ def focus_node(id:)
497
+ request = NightonaToolboxApiClient::AccessibilityNodeRequest.new(id:)
498
+ toolbox_api.focus_accessibility_node(request)
499
+ rescue StandardError => e
500
+ raise Sdk::Error, "Failed to focus accessibility node: #{e.message}"
501
+ end
502
+
503
+ # Invokes an AT-SPI accessibility node action.
504
+ #
505
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
506
+ # @param action [String, nil] Action name to invoke, or nil for the primary action
507
+ # @raise [Nightona::Sdk::Error] If the operation fails
508
+ #
509
+ # @example
510
+ # sandbox.computer_use.accessibility.invoke_node(id: node.id, action: "click")
511
+ def invoke_node(id:, action: nil)
512
+ attrs = { id: }
513
+ attrs[:action] = action unless action.nil?
514
+
515
+ request = NightonaToolboxApiClient::AccessibilityInvokeRequest.new(attrs)
516
+ toolbox_api.invoke_accessibility_node(request)
517
+ rescue StandardError => e
518
+ raise Sdk::Error, "Failed to invoke accessibility node: #{e.message}"
519
+ end
520
+
521
+ # Sets an AT-SPI accessibility node value.
522
+ #
523
+ # @param id [String] Accessibility node ID returned by get_tree or find_nodes
524
+ # @param value [String] Value to write to the node
525
+ # @raise [Nightona::Sdk::Error] If the operation fails
526
+ #
527
+ # @example
528
+ # sandbox.computer_use.accessibility.set_node_value(id: node.id, value: "hello")
529
+ def set_node_value(id:, value:)
530
+ request = NightonaToolboxApiClient::AccessibilitySetValueRequest.new(id:, value:)
531
+ toolbox_api.set_accessibility_node_value(request)
532
+ rescue StandardError => e
533
+ raise Sdk::Error, "Failed to set accessibility node value: #{e.message}"
534
+ end
535
+
536
+ instrument :get_tree, :find_nodes, :focus_node, :invoke_node, :set_node_value,
537
+ component: 'Accessibility'
538
+
539
+ private
540
+
541
+ # @return [Nightona::OtelState, nil]
542
+ attr_reader :otel_state
543
+ end
544
+
545
+ # Region coordinates for screenshot operations.
546
+ class ScreenshotRegion
547
+ # @return [Integer] X coordinate of the region
548
+ attr_accessor :x
549
+
550
+ # @return [Integer] Y coordinate of the region
551
+ attr_accessor :y
552
+
553
+ # @return [Integer] Width of the region
554
+ attr_accessor :width
555
+
556
+ # @return [Integer] Height of the region
557
+ attr_accessor :height
558
+
559
+ # @param x [Integer] X coordinate of the region
560
+ # @param y [Integer] Y coordinate of the region
561
+ # @param width [Integer] Width of the region
562
+ # @param height [Integer] Height of the region
563
+ def initialize(x:, y:, width:, height:)
564
+ @x = x
565
+ @y = y
566
+ @width = width
567
+ @height = height
568
+ end
569
+ end
570
+
571
+ # Options for screenshot compression and display.
572
+ class ScreenshotOptions
573
+ # @return [Boolean, nil] Whether to show the cursor in the screenshot
574
+ attr_accessor :show_cursor
575
+
576
+ # @return [String, nil] Image format (e.g., 'png', 'jpeg', 'webp')
577
+ attr_accessor :fmt
578
+
579
+ # @return [Integer, nil] Compression quality (0-100)
580
+ attr_accessor :quality
581
+
582
+ # @return [Float, nil] Scale factor for the screenshot
583
+ attr_accessor :scale
584
+
585
+ # @param show_cursor [Boolean, nil] Whether to show the cursor in the screenshot
586
+ # @param format [String, nil] Image format (e.g., 'png', 'jpeg', 'webp')
587
+ # @param quality [Integer, nil] Compression quality (0-100)
588
+ # @param scale [Float, nil] Scale factor for the screenshot
589
+ def initialize(show_cursor: nil, format: nil, quality: nil, scale: nil)
590
+ @show_cursor = show_cursor
591
+ @fmt = format
592
+ @quality = quality
593
+ @scale = scale
594
+ end
595
+ end
596
+
597
+ # Recording operations for computer use functionality.
598
+ class Recording
599
+ include Instrumentation
600
+
601
+ # @return [String] The ID of the sandbox
602
+ attr_reader :sandbox_id
603
+
604
+ # @return [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
605
+ attr_reader :toolbox_api
606
+
607
+ # @param sandbox_id [String] The ID of the sandbox
608
+ # @param toolbox_api [NightonaToolboxApiClient::ComputerUseApi] API client for sandbox operations
609
+ # @param otel_state [Nightona::OtelState, nil]
610
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
611
+ @sandbox_id = sandbox_id
612
+ @toolbox_api = toolbox_api
613
+ @otel_state = otel_state
614
+ end
615
+
616
+ # Starts a new screen recording session.
617
+ #
618
+ # @param label [String, nil] Optional custom label for the recording
619
+ # @return [NightonaToolboxApiClient::Recording] Started recording details
620
+ # @raise [Nightona::Sdk::Error] If the operation fails
621
+ #
622
+ # @example
623
+ # # Start a recording with a label
624
+ # recording = sandbox.computer_use.recording.start(label: "my-test-recording")
625
+ # puts "Recording started: #{recording.id}"
626
+ # puts "File: #{recording.file_path}"
627
+ def start(label: nil)
628
+ request = NightonaToolboxApiClient::StartRecordingRequest.new(label:)
629
+ toolbox_api.start_recording(request: request)
630
+ rescue StandardError => e
631
+ raise Sdk::Error, "Failed to start recording: #{e.message}"
632
+ end
633
+
634
+ # Stops an active screen recording session.
635
+ #
636
+ # @param id [String] The ID of the recording to stop
637
+ # @return [NightonaToolboxApiClient::Recording] Stopped recording details
638
+ # @raise [Nightona::Sdk::Error] If the operation fails
639
+ #
640
+ # @example
641
+ # result = sandbox.computer_use.recording.stop(id: recording.id)
642
+ # puts "Recording stopped: #{result.duration_seconds} seconds"
643
+ # puts "Saved to: #{result.file_path}"
644
+ def stop(id:)
645
+ request = NightonaToolboxApiClient::StopRecordingRequest.new(id: id)
646
+ toolbox_api.stop_recording(request)
647
+ rescue StandardError => e
648
+ raise Sdk::Error, "Failed to stop recording: #{e.message}"
649
+ end
650
+
651
+ # Lists all recordings (active and completed).
652
+ #
653
+ # @return [NightonaToolboxApiClient::ListRecordingsResponse] List of all recordings
654
+ # @raise [Nightona::Sdk::Error] If the operation fails
655
+ #
656
+ # @example
657
+ # recordings = sandbox.computer_use.recording.list
658
+ # puts "Found #{recordings.recordings.length} recordings"
659
+ # recordings.recordings.each do |rec|
660
+ # puts "- #{rec.file_name}: #{rec.status}"
661
+ # end
662
+ def list
663
+ toolbox_api.list_recordings
664
+ rescue StandardError => e
665
+ raise Sdk::Error, "Failed to list recordings: #{e.message}"
666
+ end
667
+
668
+ # Gets details of a specific recording by ID.
669
+ #
670
+ # @param id [String] The ID of the recording to retrieve
671
+ # @return [NightonaToolboxApiClient::Recording] Recording details
672
+ # @raise [Nightona::Sdk::Error] If the operation fails
673
+ #
674
+ # @example
675
+ # recording = sandbox.computer_use.recording.get(id: recording_id)
676
+ # puts "Recording: #{recording.file_name}"
677
+ # puts "Status: #{recording.status}"
678
+ # puts "Duration: #{recording.duration_seconds} seconds"
679
+ def get(id:)
680
+ toolbox_api.get_recording(id)
681
+ rescue StandardError => e
682
+ raise Sdk::Error, "Failed to get recording: #{e.message}"
683
+ end
684
+
685
+ # Deletes a recording by ID.
686
+ #
687
+ # @param id [String] The ID of the recording to delete
688
+ # @return [void]
689
+ # @raise [Nightona::Sdk::Error] If the operation fails
690
+ #
691
+ # @example
692
+ # sandbox.computer_use.recording.delete(id: recording_id)
693
+ # puts "Recording deleted"
694
+ def delete(id:)
695
+ toolbox_api.delete_recording(id)
696
+ rescue StandardError => e
697
+ raise Sdk::Error, "Failed to delete recording: #{e.message}"
698
+ end
699
+
700
+ # Downloads a recording file and saves it to a local path.
701
+ #
702
+ # The file is streamed directly to disk without loading the entire content into memory.
703
+ #
704
+ # @param id [String] The ID of the recording to download
705
+ # @param local_path [String] Path to save the recording file locally
706
+ # @return [void]
707
+ # @raise [Nightona::Sdk::Error] If the operation fails
708
+ #
709
+ # @example
710
+ # sandbox.computer_use.recording.download(id: recording_id, local_path: "local_recording.mp4")
711
+ # puts "Recording downloaded"
712
+ def download(id:, local_path:)
713
+ require 'fileutils'
714
+ require 'typhoeus'
715
+
716
+ # Get the API configuration and build the download URL
717
+ api_client = toolbox_api.api_client
718
+ config = api_client.config
719
+ base_url = config.base_url
720
+ download_url = "#{base_url}/computeruse/recordings/#{id}/download"
721
+
722
+ # Create parent directory if it doesn't exist
723
+ parent_dir = File.dirname(local_path)
724
+ FileUtils.mkdir_p(parent_dir) unless parent_dir.empty?
725
+
726
+ # Stream the download directly to file
727
+ file = File.open(local_path, 'wb')
728
+ request = Typhoeus::Request.new(
729
+ download_url,
730
+ method: :get,
731
+ headers: api_client.default_headers,
732
+ timeout: config.timeout,
733
+ ssl_verifypeer: config.verify_ssl,
734
+ ssl_verifyhost: config.verify_ssl_host ? 2 : 0
735
+ )
736
+
737
+ # Stream chunks directly to file
738
+ request.on_body do |chunk|
739
+ file.write(chunk)
740
+ end
741
+
742
+ request.on_complete do |response|
743
+ file.close
744
+ unless response.success?
745
+ File.delete(local_path) if File.exist?(local_path)
746
+ raise Sdk::Error, "Failed to download recording: HTTP #{response.code}"
747
+ end
748
+ end
749
+
750
+ request.run
751
+ rescue StandardError => e
752
+ file&.close
753
+ File.delete(local_path) if File.exist?(local_path)
754
+ raise Sdk::Error, "Failed to download recording: #{e.message}"
755
+ end
756
+
757
+ instrument :start, :stop, :list, :get, :delete, :download, component: 'Recording'
758
+
759
+ private
760
+
761
+ # @return [Nightona::OtelState, nil]
762
+ attr_reader :otel_state
763
+ end
764
+
765
+ include Instrumentation
766
+
767
+ # @return [String] The ID of the sandbox
768
+ attr_reader :sandbox_id
769
+
770
+ # @return [NightonaApiClient::ToolboxApi] API client for sandbox operations
771
+ attr_reader :toolbox_api
772
+
773
+ # @return [Mouse] Mouse operations interface
774
+ attr_reader :mouse
775
+
776
+ # @return [Keyboard] Keyboard operations interface
777
+ attr_reader :keyboard
778
+
779
+ # @return [Screenshot] Screenshot operations interface
780
+ attr_reader :screenshot
781
+
782
+ # @return [Display] Display operations interface
783
+ attr_reader :display
784
+
785
+ # @return [Recording] Screen recording operations interface
786
+ attr_reader :recording
787
+
788
+ # @return [Accessibility] Accessibility operations interface
789
+ attr_reader :accessibility
790
+
791
+ # Initialize a new ComputerUse instance.
792
+ #
793
+ # @param sandbox_id [String] The ID of the sandbox
794
+ # @param toolbox_api [NightonaApiClient::ToolboxApi] API client for sandbox operations
795
+ # @param otel_state [Nightona::OtelState, nil]
796
+ def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
797
+ @sandbox_id = sandbox_id
798
+ @toolbox_api = toolbox_api
799
+ @otel_state = otel_state
800
+ @mouse = Mouse.new(sandbox_id:, toolbox_api:, otel_state:)
801
+ @keyboard = Keyboard.new(sandbox_id:, toolbox_api:, otel_state:)
802
+ @screenshot = Screenshot.new(sandbox_id:, toolbox_api:, otel_state:)
803
+ @display = Display.new(sandbox_id:, toolbox_api:, otel_state:)
804
+ @recording = Recording.new(sandbox_id:, toolbox_api:, otel_state:)
805
+ @accessibility = Accessibility.new(sandbox_id:, toolbox_api:, otel_state:)
806
+ end
807
+
808
+ # Starts all computer use processes (Xvfb, xfce4, x11vnc, novnc).
809
+ #
810
+ # @return [NightonaApiClient::ComputerUseStartResponse] Computer use start response
811
+ # @raise [Nightona::Sdk::Error] If the operation fails
812
+ #
813
+ # @example
814
+ # result = sandbox.computer_use.start
815
+ # puts "Computer use processes started: #{result.message}"
816
+ def start
817
+ toolbox_api.start_computer_use
818
+ rescue StandardError => e
819
+ raise Sdk::Error, "Failed to start computer use: #{e.message}"
820
+ end
821
+
822
+ # Stops all computer use processes.
823
+ #
824
+ # @return [NightonaApiClient::ComputerUseStopResponse] Computer use stop response
825
+ # @raise [Nightona::Sdk::Error] If the operation fails
826
+ #
827
+ # @example
828
+ # result = sandbox.computer_use.stop
829
+ # puts "Computer use processes stopped: #{result.message}"
830
+ def stop
831
+ toolbox_api.stop_computer_use
832
+ rescue StandardError => e
833
+ raise Sdk::Error, "Failed to stop computer use: #{e.message}"
834
+ end
835
+
836
+ # Gets the status of all computer use processes.
837
+ #
838
+ # @return [NightonaApiClient::ComputerUseStatusResponse] Status information about all VNC desktop processes
839
+ # @raise [Nightona::Sdk::Error] If the operation fails
840
+ #
841
+ # @example
842
+ # response = sandbox.computer_use.get_status
843
+ # puts "Computer use status: #{response.status}"
844
+ def status
845
+ toolbox_api.get_computer_use_status
846
+ rescue StandardError => e
847
+ raise Sdk::Error, "Failed to get computer use status: #{e.message}"
848
+ end
849
+
850
+ # Gets the status of a specific VNC process.
851
+ #
852
+ # @param process_name [String] Name of the process to check
853
+ # @return [NightonaApiClient::ProcessStatusResponse] Status information about the specific process
854
+ # @raise [Nightona::Sdk::Error] If the operation fails
855
+ #
856
+ # @example
857
+ # xvfb_status = sandbox.computer_use.get_process_status("xvfb")
858
+ # no_vnc_status = sandbox.computer_use.get_process_status("novnc")
859
+ def get_process_status(process_name:)
860
+ toolbox_api.get_process_status(process_name, sandbox_id)
861
+ rescue StandardError => e
862
+ raise Sdk::Error, "Failed to get process status: #{e.message}"
863
+ end
864
+
865
+ # Restarts a specific VNC process.
866
+ #
867
+ # @param process_name [String] Name of the process to restart
868
+ # @return [NightonaApiClient::ProcessRestartResponse] Process restart response
869
+ # @raise [Nightona::Sdk::Error] If the operation fails
870
+ #
871
+ # @example
872
+ # result = sandbox.computer_use.restart_process("xfce4")
873
+ # puts "XFCE4 process restarted: #{result.message}"
874
+ def restart_process(process_name:)
875
+ toolbox_api.restart_process(process_name, sandbox_id)
876
+ rescue StandardError => e
877
+ raise Sdk::Error, "Failed to restart process: #{e.message}"
878
+ end
879
+
880
+ # Gets logs for a specific VNC process.
881
+ #
882
+ # @param process_name [String] Name of the process to get logs for
883
+ # @return [NightonaApiClient::ProcessLogsResponse] Process logs
884
+ # @raise [Nightona::Sdk::Error] If the operation fails
885
+ #
886
+ # @example
887
+ # logs = sandbox.computer_use.get_process_logs("novnc")
888
+ # puts "NoVNC logs: #{logs}"
889
+ def get_process_logs(process_name:)
890
+ toolbox_api.get_process_logs(process_name, sandbox_id)
891
+ rescue StandardError => e
892
+ raise Sdk::Error, "Failed to get process logs: #{e.message}"
893
+ end
894
+
895
+ # Gets error logs for a specific VNC process.
896
+ #
897
+ # @param process_name [String] Name of the process to get error logs for
898
+ # @return [NightonaApiClient::ProcessErrorsResponse] Process error logs
899
+ # @raise [Nightona::Sdk::Error] If the operation fails
900
+ #
901
+ # @example
902
+ # errors = sandbox.computer_use.get_process_errors("x11vnc")
903
+ # puts "X11VNC errors: #{errors}"
904
+ def get_process_errors(process_name:)
905
+ toolbox_api.get_process_errors(process_name, sandbox_id)
906
+ rescue StandardError => e
907
+ raise Sdk::Error, "Failed to get process errors: #{e.message}"
908
+ end
909
+
910
+ instrument :start, :stop, :status, :get_process_status, :restart_process,
911
+ :get_process_logs, :get_process_errors,
912
+ component: 'ComputerUse'
913
+
914
+ private
915
+
916
+ # @return [Nightona::OtelState, nil]
917
+ attr_reader :otel_state
918
+ end
919
+ end