stagehand 0.0.4 → 3.6.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 (148) hide show
  1. checksums.yaml +7 -0
  2. data/.ignore +2 -0
  3. data/CHANGELOG.md +193 -0
  4. data/README.md +403 -31
  5. data/SECURITY.md +23 -0
  6. data/lib/stagehand/client.rb +123 -4
  7. data/lib/stagehand/errors.rb +228 -0
  8. data/lib/stagehand/file_part.rb +58 -0
  9. data/lib/stagehand/internal/stream.rb +56 -0
  10. data/lib/stagehand/internal/transport/base_client.rb +575 -0
  11. data/lib/stagehand/internal/transport/pooled_net_requester.rb +210 -0
  12. data/lib/stagehand/internal/type/array_of.rb +168 -0
  13. data/lib/stagehand/internal/type/base_model.rb +531 -0
  14. data/lib/stagehand/internal/type/base_page.rb +55 -0
  15. data/lib/stagehand/internal/type/base_stream.rb +83 -0
  16. data/lib/stagehand/internal/type/boolean.rb +77 -0
  17. data/lib/stagehand/internal/type/converter.rb +327 -0
  18. data/lib/stagehand/internal/type/enum.rb +131 -0
  19. data/lib/stagehand/internal/type/file_input.rb +111 -0
  20. data/lib/stagehand/internal/type/hash_of.rb +188 -0
  21. data/lib/stagehand/internal/type/request_parameters.rb +42 -0
  22. data/lib/stagehand/internal/type/union.rb +237 -0
  23. data/lib/stagehand/internal/type/unknown.rb +81 -0
  24. data/lib/stagehand/internal/util.rb +920 -0
  25. data/lib/stagehand/internal.rb +20 -0
  26. data/lib/stagehand/local.rb +439 -0
  27. data/lib/stagehand/models/action.rb +50 -0
  28. data/lib/stagehand/models/model_config.rb +55 -0
  29. data/lib/stagehand/models/session_act_params.rb +112 -0
  30. data/lib/stagehand/models/session_act_response.rb +127 -0
  31. data/lib/stagehand/models/session_end_params.rb +33 -0
  32. data/lib/stagehand/models/session_end_response.rb +17 -0
  33. data/lib/stagehand/models/session_execute_params.rb +212 -0
  34. data/lib/stagehand/models/session_execute_response.rb +212 -0
  35. data/lib/stagehand/models/session_extract_params.rb +107 -0
  36. data/lib/stagehand/models/session_extract_response.rb +46 -0
  37. data/lib/stagehand/models/session_navigate_params.rb +107 -0
  38. data/lib/stagehand/models/session_navigate_response.rb +44 -0
  39. data/lib/stagehand/models/session_observe_params.rb +99 -0
  40. data/lib/stagehand/models/session_observe_response.rb +91 -0
  41. data/lib/stagehand/models/session_replay_params.rb +33 -0
  42. data/lib/stagehand/models/session_replay_response.rb +142 -0
  43. data/lib/stagehand/models/session_start_params.rb +762 -0
  44. data/lib/stagehand/models/session_start_response.rb +55 -0
  45. data/lib/stagehand/models/stream_event.rb +120 -0
  46. data/lib/stagehand/models.rb +63 -0
  47. data/lib/stagehand/request_options.rb +77 -0
  48. data/lib/stagehand/resources/sessions.rb +488 -0
  49. data/lib/stagehand/version.rb +3 -1
  50. data/lib/stagehand.rb +74 -29
  51. data/manifest.yaml +17 -0
  52. data/rbi/stagehand/client.rbi +89 -0
  53. data/rbi/stagehand/errors.rbi +205 -0
  54. data/rbi/stagehand/file_part.rbi +37 -0
  55. data/rbi/stagehand/internal/stream.rbi +20 -0
  56. data/rbi/stagehand/internal/transport/base_client.rbi +314 -0
  57. data/rbi/stagehand/internal/transport/pooled_net_requester.rbi +83 -0
  58. data/rbi/stagehand/internal/type/array_of.rbi +104 -0
  59. data/rbi/stagehand/internal/type/base_model.rbi +308 -0
  60. data/rbi/stagehand/internal/type/base_page.rbi +42 -0
  61. data/rbi/stagehand/internal/type/base_stream.rbi +75 -0
  62. data/rbi/stagehand/internal/type/boolean.rbi +58 -0
  63. data/rbi/stagehand/internal/type/converter.rbi +216 -0
  64. data/rbi/stagehand/internal/type/enum.rbi +82 -0
  65. data/rbi/stagehand/internal/type/file_input.rbi +59 -0
  66. data/rbi/stagehand/internal/type/hash_of.rbi +104 -0
  67. data/rbi/stagehand/internal/type/request_parameters.rbi +29 -0
  68. data/rbi/stagehand/internal/type/union.rbi +128 -0
  69. data/rbi/stagehand/internal/type/unknown.rbi +58 -0
  70. data/rbi/stagehand/internal/util.rbi +487 -0
  71. data/rbi/stagehand/internal.rbi +18 -0
  72. data/rbi/stagehand/models/action.rbi +77 -0
  73. data/rbi/stagehand/models/model_config.rbi +94 -0
  74. data/rbi/stagehand/models/session_act_params.rbi +204 -0
  75. data/rbi/stagehand/models/session_act_response.rbi +250 -0
  76. data/rbi/stagehand/models/session_end_params.rbi +87 -0
  77. data/rbi/stagehand/models/session_end_response.rbi +30 -0
  78. data/rbi/stagehand/models/session_execute_params.rbi +440 -0
  79. data/rbi/stagehand/models/session_execute_response.rbi +414 -0
  80. data/rbi/stagehand/models/session_extract_params.rbi +209 -0
  81. data/rbi/stagehand/models/session_extract_response.rbi +91 -0
  82. data/rbi/stagehand/models/session_navigate_params.rbi +240 -0
  83. data/rbi/stagehand/models/session_navigate_response.rbi +91 -0
  84. data/rbi/stagehand/models/session_observe_params.rbi +198 -0
  85. data/rbi/stagehand/models/session_observe_response.rbi +184 -0
  86. data/rbi/stagehand/models/session_replay_params.rbi +89 -0
  87. data/rbi/stagehand/models/session_replay_response.rbi +303 -0
  88. data/rbi/stagehand/models/session_start_params.rbi +1703 -0
  89. data/rbi/stagehand/models/session_start_response.rbi +102 -0
  90. data/rbi/stagehand/models/stream_event.rbi +237 -0
  91. data/rbi/stagehand/models.rbi +25 -0
  92. data/rbi/stagehand/request_options.rbi +59 -0
  93. data/rbi/stagehand/resources/sessions.rbi +421 -0
  94. data/rbi/stagehand/version.rbi +5 -0
  95. data/sig/stagehand/client.rbs +41 -0
  96. data/sig/stagehand/errors.rbs +117 -0
  97. data/sig/stagehand/file_part.rbs +21 -0
  98. data/sig/stagehand/internal/stream.rbs +9 -0
  99. data/sig/stagehand/internal/transport/base_client.rbs +133 -0
  100. data/sig/stagehand/internal/transport/pooled_net_requester.rbs +48 -0
  101. data/sig/stagehand/internal/type/array_of.rbs +48 -0
  102. data/sig/stagehand/internal/type/base_model.rbs +102 -0
  103. data/sig/stagehand/internal/type/base_page.rbs +24 -0
  104. data/sig/stagehand/internal/type/base_stream.rbs +38 -0
  105. data/sig/stagehand/internal/type/boolean.rbs +26 -0
  106. data/sig/stagehand/internal/type/converter.rbs +79 -0
  107. data/sig/stagehand/internal/type/enum.rbs +32 -0
  108. data/sig/stagehand/internal/type/file_input.rbs +25 -0
  109. data/sig/stagehand/internal/type/hash_of.rbs +48 -0
  110. data/sig/stagehand/internal/type/request_parameters.rbs +19 -0
  111. data/sig/stagehand/internal/type/union.rbs +52 -0
  112. data/sig/stagehand/internal/type/unknown.rbs +26 -0
  113. data/sig/stagehand/internal/util.rbs +185 -0
  114. data/sig/stagehand/internal.rbs +9 -0
  115. data/sig/stagehand/models/action.rbs +46 -0
  116. data/sig/stagehand/models/model_config.rbs +56 -0
  117. data/sig/stagehand/models/session_act_params.rbs +111 -0
  118. data/sig/stagehand/models/session_act_response.rbs +121 -0
  119. data/sig/stagehand/models/session_end_params.rbs +41 -0
  120. data/sig/stagehand/models/session_end_response.rbs +13 -0
  121. data/sig/stagehand/models/session_execute_params.rbs +193 -0
  122. data/sig/stagehand/models/session_execute_response.rbs +215 -0
  123. data/sig/stagehand/models/session_extract_params.rbs +112 -0
  124. data/sig/stagehand/models/session_extract_response.rbs +36 -0
  125. data/sig/stagehand/models/session_navigate_params.rbs +114 -0
  126. data/sig/stagehand/models/session_navigate_response.rbs +36 -0
  127. data/sig/stagehand/models/session_observe_params.rbs +105 -0
  128. data/sig/stagehand/models/session_observe_response.rbs +89 -0
  129. data/sig/stagehand/models/session_replay_params.rbs +41 -0
  130. data/sig/stagehand/models/session_replay_response.rbs +166 -0
  131. data/sig/stagehand/models/session_start_params.rbs +866 -0
  132. data/sig/stagehand/models/session_start_response.rbs +44 -0
  133. data/sig/stagehand/models/stream_event.rbs +109 -0
  134. data/sig/stagehand/models.rbs +23 -0
  135. data/sig/stagehand/request_options.rbs +34 -0
  136. data/sig/stagehand/resources/sessions.rbs +121 -0
  137. data/sig/stagehand/version.rbs +3 -0
  138. metadata +170 -54
  139. data/.gitignore +0 -16
  140. data/Gemfile +0 -4
  141. data/Rakefile +0 -10
  142. data/lib/stagehand/client/oauth.rb +0 -32
  143. data/lib/stagehand/client/user.rb +0 -10
  144. data/lib/stagehand/rack/middleware.rb +0 -33
  145. data/lib/stagehand/railtie.rb +0 -19
  146. data/spec/spec_helper.rb +0 -7
  147. data/spec/stagehand_spec.rb +0 -44
  148. data/stagehand.gemspec +0 -25
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagehand
4
+ module Internal
5
+ extend Stagehand::Internal::Util::SorbetRuntimeSupport
6
+
7
+ OMIT =
8
+ Object.new.tap do
9
+ _1.define_singleton_method(:inspect) { "#<#{Stagehand::Internal}::OMIT>" }
10
+ end
11
+ .freeze
12
+
13
+ define_sorbet_constant!(:AnyHash) do
14
+ T.type_alias { T::Hash[Symbol, T.anything] }
15
+ end
16
+ define_sorbet_constant!(:FileInput) do
17
+ T.type_alias { T.any(Pathname, StringIO, IO, String, Stagehand::FilePart) }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Custom code. Not generated by Stainless.
4
+ require "fileutils"
5
+ require "json"
6
+ require "net/http"
7
+ require "socket"
8
+ require "tmpdir"
9
+
10
+ module Stagehand
11
+ module Local
12
+ STAGEHAND_REPO = "browserbase/stagehand"
13
+ DEFAULT_USER_AGENT = "stagehand-ruby/local"
14
+ DEFAULT_HOST = "127.0.0.1"
15
+ DEFAULT_READY_TIMEOUT_S = 30.0
16
+ DOWNLOAD_TIMEOUT_S = 600
17
+
18
+ @download_mutex = Mutex.new
19
+
20
+ def self.download_mutex
21
+ @download_mutex
22
+ end
23
+
24
+ def self.local_mode?(client)
25
+ client.instance_variable_get(:@server_mode).to_s == "local"
26
+ end
27
+
28
+ def self.browser_type_from_params(params)
29
+ return nil unless params.is_a?(Hash)
30
+
31
+ browser = params[:browser] || params["browser"]
32
+ return nil if browser.nil?
33
+
34
+ type =
35
+ case browser
36
+ when Hash
37
+ browser[:type] || browser["type"]
38
+ else
39
+ browser.respond_to?(:type) ? browser.type : nil
40
+ end
41
+
42
+ type&.to_sym
43
+ end
44
+
45
+ def self.ensure_browserbase_credentials!(client:, params:)
46
+ return unless local_mode?(client)
47
+
48
+ browser_type = browser_type_from_params(params)
49
+ return if browser_type == :local
50
+
51
+ missing = []
52
+ missing << "browserbase_api_key" if client.browserbase_api_key.to_s.empty?
53
+ missing << "browserbase_project_id" if client.browserbase_project_id.to_s.empty?
54
+ return if missing.empty?
55
+
56
+ message =
57
+ "Browserbase credentials are required when launching a Browserbase browser: " \
58
+ "missing #{missing.join(', ')}."
59
+ raise ArgumentError, message
60
+ end
61
+
62
+ def self.windows?
63
+ RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/i
64
+ end
65
+
66
+ def self.macos?
67
+ RbConfig::CONFIG["host_os"] =~ /darwin/i
68
+ end
69
+
70
+ module Binary
71
+ module_function
72
+
73
+ def platform_tag
74
+ platform = if Stagehand::Local.macos?
75
+ "darwin"
76
+ elsif Stagehand::Local.windows?
77
+ "win32"
78
+ else
79
+ "linux"
80
+ end
81
+ cpu = RbConfig::CONFIG["host_cpu"].to_s.downcase
82
+ arch = cpu.include?("arm") || cpu.include?("aarch64") ? "arm64" : "x64"
83
+ [platform, arch]
84
+ end
85
+
86
+ def binary_filename
87
+ platform, arch = platform_tag
88
+ name = "stagehand-server-#{platform}-#{arch}"
89
+ name += ".exe" if platform == "win32"
90
+ name
91
+ end
92
+
93
+ def resolve_binary_path
94
+ env = ENV["STAGEHAND_SEA_BINARY"].to_s
95
+ return ensure_executable(env) unless env.empty?
96
+
97
+ filename = binary_filename
98
+ cache_path = File.join(cache_dir, filename)
99
+ return ensure_executable(cache_path) if File.file?(cache_path)
100
+
101
+ Stagehand::Local.download_mutex.synchronize do
102
+ return ensure_executable(cache_path) if File.file?(cache_path)
103
+
104
+ version = ENV["STAGEHAND_SERVER_VERSION"].to_s
105
+ version = "latest" if version.empty?
106
+ tag = resolve_version(version)
107
+ download_binary(tag, cache_path)
108
+ end
109
+
110
+ ensure_executable(cache_path)
111
+ end
112
+
113
+ def cache_dir
114
+ root =
115
+ if Stagehand::Local.macos?
116
+ File.join(home_dir || Dir.tmpdir, "Library", "Caches")
117
+ elsif Stagehand::Local.windows?
118
+ ENV["LOCALAPPDATA"] || File.join(home_dir || Dir.tmpdir, "AppData", "Local")
119
+ else
120
+ ENV["XDG_CACHE_HOME"] || File.join(home_dir || Dir.tmpdir, ".cache")
121
+ end
122
+ version = Stagehand::VERSION.to_s
123
+ File.join(root, "stagehand", "lib", "ruby_#{version}")
124
+ end
125
+
126
+ def home_dir
127
+ Dir.home
128
+ rescue StandardError
129
+ nil
130
+ end
131
+
132
+ def resolve_version(version)
133
+ return fetch_latest_tag if version.empty? || version == "latest"
134
+ return version if version.start_with?("stagehand-server/")
135
+
136
+ "stagehand-server/#{version}"
137
+ end
138
+
139
+ def fetch_latest_tag
140
+ url = URI("https://api.github.com/repos/#{STAGEHAND_REPO}/releases?per_page=15")
141
+ request = Net::HTTP::Get.new(url)
142
+ request["User-Agent"] = DEFAULT_USER_AGENT
143
+ response = http_request(url, request)
144
+ unless response.is_a?(Net::HTTPSuccess)
145
+ raise "Failed to fetch releases: #{response.code} #{response.message}"
146
+ end
147
+
148
+ releases = JSON.parse(response.body.to_s)
149
+ releases.each do |release|
150
+ tag = release["tag_name"]
151
+ return tag if tag.is_a?(String) && tag.start_with?("stagehand-server/")
152
+ end
153
+ raise "Failed to find stagehand-server release tag"
154
+ end
155
+
156
+ def download_binary(tag, dest_path)
157
+ filename = binary_filename
158
+ url = URI("https://github.com/#{STAGEHAND_REPO}/releases/download/#{tag}/#{filename}")
159
+ FileUtils.mkdir_p(File.dirname(dest_path))
160
+
161
+ tmp_path = "#{dest_path}.tmp"
162
+ download_with_redirects(url, tmp_path, limit: 3)
163
+ FileUtils.mv(tmp_path, dest_path)
164
+ ensure_executable(dest_path)
165
+ rescue StandardError => e
166
+ FileUtils.rm_f(tmp_path)
167
+ hint = manual_download_hint(filename, dest_path)
168
+ raise "Failed to download Stagehand driver binary: #{e.message}. #{hint}"
169
+ end
170
+
171
+ def manual_download_hint(filename, dest_path)
172
+ "Download #{filename} from https://github.com/#{STAGEHAND_REPO}/releases " \
173
+ "and save it to: #{dest_path}."
174
+ end
175
+
176
+ def download_with_redirects(url, dest_path, limit:)
177
+ raise "Too many redirects while downloading Stagehand driver binary." if limit <= 0
178
+
179
+ request = Net::HTTP::Get.new(url)
180
+ request["User-Agent"] = DEFAULT_USER_AGENT
181
+ response = http_request(url, request)
182
+
183
+ case response
184
+ when Net::HTTPRedirection
185
+ location = response["location"]
186
+ raise "Missing redirect location." if location.nil?
187
+
188
+ download_with_redirects(URI(location), dest_path, limit: limit - 1)
189
+ when Net::HTTPSuccess
190
+ File.binwrite(dest_path, response.body)
191
+ nil
192
+ else
193
+ raise "Failed to download binary: #{response.code} #{response.message}"
194
+ end
195
+ end
196
+
197
+ def http_request(url, request)
198
+ Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == "https") do |http|
199
+ http.open_timeout = 30
200
+ http.read_timeout = DOWNLOAD_TIMEOUT_S
201
+ http.request(request)
202
+ end
203
+ end
204
+
205
+ def ensure_executable(path)
206
+ return path if Stagehand::Local.windows?
207
+ return path unless File.exist?(path)
208
+
209
+ mode = File.stat(path).mode
210
+ File.chmod(mode | 0o100, path)
211
+ path
212
+ end
213
+ end
214
+
215
+ class ServerManager
216
+ def initialize(model_api_key:, browserbase_api_key:, browserbase_project_id:)
217
+ @model_api_key = model_api_key
218
+ @browserbase_api_key = browserbase_api_key
219
+ @browserbase_project_id = browserbase_project_id
220
+ @host = DEFAULT_HOST
221
+ @port = 0
222
+ @mutex = Mutex.new
223
+ @pid = nil
224
+ @base_url = nil
225
+ @binary_path = nil
226
+ @pgroup = !Stagehand::Local.windows?
227
+ @at_exit_registered = false
228
+ end
229
+
230
+ def ensure_running
231
+ @mutex.synchronize do
232
+ return @base_url if running? && @base_url
233
+
234
+ start
235
+ end
236
+ end
237
+
238
+ def close
239
+ @mutex.synchronize do
240
+ return if @pid.nil?
241
+
242
+ terminate(@pid)
243
+ @pid = nil
244
+ @base_url = nil
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ def start
251
+ @binary_path ||= Stagehand::Local::Binary.resolve_binary_path
252
+ port = @port.zero? ? pick_free_port(@host) : @port
253
+ base_url = "http://#{@host}:#{port}"
254
+
255
+ env = build_env(host: @host, port: port)
256
+ spawn_opts = {out: $stdout, err: $stderr}
257
+ spawn_opts[:pgroup] = true if @pgroup
258
+
259
+ @pid = Process.spawn(env, @binary_path, **spawn_opts)
260
+ register_at_exit_once
261
+
262
+ begin
263
+ wait_ready(base_url, timeout_s: DEFAULT_READY_TIMEOUT_S)
264
+ rescue StandardError
265
+ terminate(@pid)
266
+ @pid = nil
267
+ raise
268
+ end
269
+
270
+ @base_url = base_url
271
+ end
272
+
273
+ def running?
274
+ return false if @pid.nil?
275
+
276
+ Process.kill(0, @pid)
277
+ true
278
+ rescue Errno::ESRCH
279
+ false
280
+ rescue Errno::EPERM
281
+ true
282
+ end
283
+
284
+ def register_at_exit_once
285
+ return if @at_exit_registered
286
+
287
+ at_exit { close }
288
+ @at_exit_registered = true
289
+ end
290
+
291
+ def terminate(pid)
292
+ return if pid.nil?
293
+
294
+ target = @pgroup ? -pid : pid
295
+ begin
296
+ Process.kill("TERM", target)
297
+ rescue StandardError
298
+ nil
299
+ end
300
+
301
+ return if wait_for_exit(pid, timeout_s: 3.0)
302
+
303
+ begin
304
+ Process.kill("KILL", target)
305
+ rescue StandardError
306
+ nil
307
+ end
308
+ wait_for_exit(pid, timeout_s: 3.0)
309
+ end
310
+
311
+ def wait_for_exit(pid, timeout_s:)
312
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_s
313
+ loop do
314
+ result = Process.waitpid(pid, Process::WNOHANG)
315
+ return true if result
316
+ return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
317
+
318
+ sleep(0.1)
319
+ end
320
+ rescue Errno::ECHILD
321
+ true
322
+ end
323
+
324
+ def pick_free_port(host)
325
+ TCPServer.open(host, 0) { |server| server.addr[1] }
326
+ end
327
+
328
+ def build_env(host:, port:)
329
+ env = ENV.to_h
330
+ env["NODE_ENV"] = "production"
331
+ env["BB_ENV"] = "local"
332
+ env["HOST"] = host
333
+ env["PORT"] = port.to_s
334
+ env["MODEL_API_KEY"] = @model_api_key if @model_api_key.to_s != ""
335
+ if @browserbase_api_key.to_s != ""
336
+ env["BROWSERBASE_API_KEY"] = @browserbase_api_key
337
+ end
338
+ if @browserbase_project_id.to_s != ""
339
+ env["BROWSERBASE_PROJECT_ID"] = @browserbase_project_id
340
+ end
341
+ env
342
+ end
343
+
344
+ def wait_ready(base_url, timeout_s:)
345
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_s
346
+ paths = ["/readyz", "/healthz", "/health"]
347
+
348
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
349
+ raise "Stagehand local server exited unexpectedly" unless running?
350
+
351
+ return if paths.any? { |path| ready_path?(base_url, path) }
352
+ sleep(0.1)
353
+ end
354
+
355
+ raise "Stagehand local server not ready at #{base_url} after #{timeout_s}s"
356
+ end
357
+
358
+ def ready_path?(base_url, path)
359
+ uri = URI.join(base_url, path)
360
+ http = Net::HTTP.new(uri.host, uri.port)
361
+ http.open_timeout = 1
362
+ http.read_timeout = 1
363
+ response = http.get(uri.request_uri)
364
+ response.is_a?(Net::HTTPSuccess)
365
+ rescue StandardError
366
+ false
367
+ end
368
+ end
369
+
370
+ module ClientPatch
371
+ def initialize(server: "remote", **kwargs)
372
+ @server_mode = server.to_s
373
+
374
+ if @server_mode == "local"
375
+ base_url = kwargs[:base_url]
376
+ kwargs[:base_url] = base_url.nil? ? "http://#{DEFAULT_HOST}" : base_url
377
+ kwargs[:browserbase_api_key] =
378
+ kwargs[:browserbase_api_key] || ENV["BROWSERBASE_API_KEY"] || ""
379
+ kwargs[:browserbase_project_id] =
380
+ kwargs[:browserbase_project_id] || ENV["BROWSERBASE_PROJECT_ID"] || ""
381
+ end
382
+
383
+ super(**kwargs)
384
+
385
+ return unless @server_mode == "local"
386
+
387
+ @local_server_manager = Stagehand::Local::ServerManager.new(
388
+ model_api_key: @model_api_key,
389
+ browserbase_api_key: @browserbase_api_key,
390
+ browserbase_project_id: @browserbase_project_id
391
+ )
392
+ end
393
+
394
+ def request(req)
395
+ ensure_local_server!
396
+ super
397
+ end
398
+
399
+ def close
400
+ super if defined?(super)
401
+ ensure
402
+ @local_server_manager&.close
403
+ end
404
+
405
+ private
406
+
407
+ def ensure_local_server!
408
+ return unless @server_mode == "local"
409
+ return if @local_server_manager.nil?
410
+
411
+ base_url = @local_server_manager.ensure_running
412
+ return if @base_url.to_s == base_url
413
+
414
+ @base_url_components = Stagehand::Internal::Util.parse_uri(base_url)
415
+ @base_url = Stagehand::Internal::Util.unparse_uri(@base_url_components)
416
+ end
417
+
418
+ def bb_api_key_auth
419
+ return {} if @browserbase_api_key.to_s.empty?
420
+ super
421
+ end
422
+
423
+ def bb_project_id_auth
424
+ return {} if @browserbase_project_id.to_s.empty?
425
+ super
426
+ end
427
+ end
428
+
429
+ module SessionsPatch
430
+ def start(params)
431
+ Stagehand::Local.ensure_browserbase_credentials!(client: @client, params: params)
432
+ super
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ Stagehand::Client.prepend(Stagehand::Local::ClientPatch) if defined?(Stagehand::Client)
439
+ Stagehand::Resources::Sessions.prepend(Stagehand::Local::SessionsPatch) if defined?(Stagehand::Resources::Sessions)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagehand
4
+ module Models
5
+ class Action < Stagehand::Internal::Type::BaseModel
6
+ # @!attribute description
7
+ # Human-readable description of the action
8
+ #
9
+ # @return [String]
10
+ required :description, String
11
+
12
+ # @!attribute selector
13
+ # CSS selector or XPath for the element
14
+ #
15
+ # @return [String]
16
+ required :selector, String
17
+
18
+ # @!attribute arguments
19
+ # Arguments to pass to the method
20
+ #
21
+ # @return [Array<String>, nil]
22
+ optional :arguments, Stagehand::Internal::Type::ArrayOf[String]
23
+
24
+ # @!attribute backend_node_id
25
+ # Backend node ID for the element
26
+ #
27
+ # @return [Float, nil]
28
+ optional :backend_node_id, Float, api_name: :backendNodeId
29
+
30
+ # @!attribute method_
31
+ # The method to execute (click, fill, etc.)
32
+ #
33
+ # @return [String, nil]
34
+ optional :method_, String, api_name: :method
35
+
36
+ # @!method initialize(description:, selector:, arguments: nil, backend_node_id: nil, method_: nil)
37
+ # Action object returned by observe and used by act
38
+ #
39
+ # @param description [String] Human-readable description of the action
40
+ #
41
+ # @param selector [String] CSS selector or XPath for the element
42
+ #
43
+ # @param arguments [Array<String>] Arguments to pass to the method
44
+ #
45
+ # @param backend_node_id [Float] Backend node ID for the element
46
+ #
47
+ # @param method_ [String] The method to execute (click, fill, etc.)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagehand
4
+ module Models
5
+ class ModelConfig < Stagehand::Internal::Type::BaseModel
6
+ # @!attribute model_name
7
+ # Model name string with provider prefix (e.g., 'openai/gpt-5-nano')
8
+ #
9
+ # @return [String]
10
+ required :model_name, String, api_name: :modelName
11
+
12
+ # @!attribute api_key
13
+ # API key for the model provider
14
+ #
15
+ # @return [String, nil]
16
+ optional :api_key, String, api_name: :apiKey
17
+
18
+ # @!attribute base_url
19
+ # Base URL for the model provider
20
+ #
21
+ # @return [String, nil]
22
+ optional :base_url, String, api_name: :baseURL
23
+
24
+ # @!attribute provider
25
+ # AI provider for the model (or provide a baseURL endpoint instead)
26
+ #
27
+ # @return [Symbol, Stagehand::Models::ModelConfig::Provider, nil]
28
+ optional :provider, enum: -> { Stagehand::ModelConfig::Provider }
29
+
30
+ # @!method initialize(model_name:, api_key: nil, base_url: nil, provider: nil)
31
+ # @param model_name [String] Model name string with provider prefix (e.g., 'openai/gpt-5-nano')
32
+ #
33
+ # @param api_key [String] API key for the model provider
34
+ #
35
+ # @param base_url [String] Base URL for the model provider
36
+ #
37
+ # @param provider [Symbol, Stagehand::Models::ModelConfig::Provider] AI provider for the model (or provide a baseURL endpoint instead)
38
+
39
+ # AI provider for the model (or provide a baseURL endpoint instead)
40
+ #
41
+ # @see Stagehand::Models::ModelConfig#provider
42
+ module Provider
43
+ extend Stagehand::Internal::Type::Enum
44
+
45
+ OPENAI = :openai
46
+ ANTHROPIC = :anthropic
47
+ GOOGLE = :google
48
+ MICROSOFT = :microsoft
49
+
50
+ # @!method self.values
51
+ # @return [Array<Symbol>]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagehand
4
+ module Models
5
+ # @see Stagehand::Resources::Sessions#act
6
+ #
7
+ # @see Stagehand::Resources::Sessions#act_streaming
8
+ class SessionActParams < Stagehand::Internal::Type::BaseModel
9
+ extend Stagehand::Internal::Type::RequestParameters::Converter
10
+ include Stagehand::Internal::Type::RequestParameters
11
+
12
+ # @!attribute input
13
+ # Natural language instruction or Action object
14
+ #
15
+ # @return [String, Stagehand::Models::Action]
16
+ required :input, union: -> { Stagehand::SessionActParams::Input }
17
+
18
+ # @!attribute frame_id
19
+ # Target frame ID for the action
20
+ #
21
+ # @return [String, nil]
22
+ optional :frame_id, String, api_name: :frameId, nil?: true
23
+
24
+ # @!attribute options
25
+ #
26
+ # @return [Stagehand::Models::SessionActParams::Options, nil]
27
+ optional :options, -> { Stagehand::SessionActParams::Options }
28
+
29
+ # @!attribute x_stream_response
30
+ # Whether to stream the response via SSE
31
+ #
32
+ # @return [Symbol, Stagehand::Models::SessionActParams::XStreamResponse, nil]
33
+ optional :x_stream_response, enum: -> { Stagehand::SessionActParams::XStreamResponse }
34
+
35
+ # @!method initialize(input:, frame_id: nil, options: nil, x_stream_response: nil, request_options: {})
36
+ # @param input [String, Stagehand::Models::Action] Natural language instruction or Action object
37
+ #
38
+ # @param frame_id [String, nil] Target frame ID for the action
39
+ #
40
+ # @param options [Stagehand::Models::SessionActParams::Options]
41
+ #
42
+ # @param x_stream_response [Symbol, Stagehand::Models::SessionActParams::XStreamResponse] Whether to stream the response via SSE
43
+ #
44
+ # @param request_options [Stagehand::RequestOptions, Hash{Symbol=>Object}]
45
+
46
+ # Natural language instruction or Action object
47
+ module Input
48
+ extend Stagehand::Internal::Type::Union
49
+
50
+ variant String
51
+
52
+ # Action object returned by observe and used by act
53
+ variant -> { Stagehand::Action }
54
+
55
+ # @!method self.variants
56
+ # @return [Array(String, Stagehand::Models::Action)]
57
+ end
58
+
59
+ class Options < Stagehand::Internal::Type::BaseModel
60
+ # @!attribute model
61
+ # Model configuration object or model name string (e.g., 'openai/gpt-5-nano')
62
+ #
63
+ # @return [Stagehand::Models::ModelConfig, String, nil]
64
+ optional :model, union: -> { Stagehand::SessionActParams::Options::Model }
65
+
66
+ # @!attribute timeout
67
+ # Timeout in ms for the action
68
+ #
69
+ # @return [Float, nil]
70
+ optional :timeout, Float
71
+
72
+ # @!attribute variables
73
+ # Variables to substitute in the action instruction
74
+ #
75
+ # @return [Hash{Symbol=>String}, nil]
76
+ optional :variables, Stagehand::Internal::Type::HashOf[String]
77
+
78
+ # @!method initialize(model: nil, timeout: nil, variables: nil)
79
+ # @param model [Stagehand::Models::ModelConfig, String] Model configuration object or model name string (e.g., 'openai/gpt-5-nano')
80
+ #
81
+ # @param timeout [Float] Timeout in ms for the action
82
+ #
83
+ # @param variables [Hash{Symbol=>String}] Variables to substitute in the action instruction
84
+
85
+ # Model configuration object or model name string (e.g., 'openai/gpt-5-nano')
86
+ #
87
+ # @see Stagehand::Models::SessionActParams::Options#model
88
+ module Model
89
+ extend Stagehand::Internal::Type::Union
90
+
91
+ variant -> { Stagehand::ModelConfig }
92
+
93
+ variant String
94
+
95
+ # @!method self.variants
96
+ # @return [Array(Stagehand::Models::ModelConfig, String)]
97
+ end
98
+ end
99
+
100
+ # Whether to stream the response via SSE
101
+ module XStreamResponse
102
+ extend Stagehand::Internal::Type::Enum
103
+
104
+ TRUE = :true
105
+ FALSE = :false
106
+
107
+ # @!method self.values
108
+ # @return [Array<Symbol>]
109
+ end
110
+ end
111
+ end
112
+ end