stagehand 0.0.4 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ignore +2 -0
- data/CHANGELOG.md +185 -0
- data/README.md +394 -31
- data/SECURITY.md +23 -0
- data/lib/stagehand/client.rb +123 -4
- data/lib/stagehand/errors.rb +228 -0
- data/lib/stagehand/file_part.rb +58 -0
- data/lib/stagehand/internal/stream.rb +56 -0
- data/lib/stagehand/internal/transport/base_client.rb +575 -0
- data/lib/stagehand/internal/transport/pooled_net_requester.rb +210 -0
- data/lib/stagehand/internal/type/array_of.rb +168 -0
- data/lib/stagehand/internal/type/base_model.rb +531 -0
- data/lib/stagehand/internal/type/base_page.rb +55 -0
- data/lib/stagehand/internal/type/base_stream.rb +83 -0
- data/lib/stagehand/internal/type/boolean.rb +77 -0
- data/lib/stagehand/internal/type/converter.rb +327 -0
- data/lib/stagehand/internal/type/enum.rb +131 -0
- data/lib/stagehand/internal/type/file_input.rb +111 -0
- data/lib/stagehand/internal/type/hash_of.rb +188 -0
- data/lib/stagehand/internal/type/request_parameters.rb +42 -0
- data/lib/stagehand/internal/type/union.rb +237 -0
- data/lib/stagehand/internal/type/unknown.rb +81 -0
- data/lib/stagehand/internal/util.rb +920 -0
- data/lib/stagehand/internal.rb +20 -0
- data/lib/stagehand/local.rb +439 -0
- data/lib/stagehand/models/action.rb +50 -0
- data/lib/stagehand/models/model_config.rb +55 -0
- data/lib/stagehand/models/session_act_params.rb +112 -0
- data/lib/stagehand/models/session_act_response.rb +127 -0
- data/lib/stagehand/models/session_end_params.rb +33 -0
- data/lib/stagehand/models/session_end_response.rb +17 -0
- data/lib/stagehand/models/session_execute_params.rb +212 -0
- data/lib/stagehand/models/session_execute_response.rb +212 -0
- data/lib/stagehand/models/session_extract_params.rb +107 -0
- data/lib/stagehand/models/session_extract_response.rb +46 -0
- data/lib/stagehand/models/session_navigate_params.rb +107 -0
- data/lib/stagehand/models/session_navigate_response.rb +44 -0
- data/lib/stagehand/models/session_observe_params.rb +99 -0
- data/lib/stagehand/models/session_observe_response.rb +91 -0
- data/lib/stagehand/models/session_replay_params.rb +33 -0
- data/lib/stagehand/models/session_replay_response.rb +100 -0
- data/lib/stagehand/models/session_start_params.rb +762 -0
- data/lib/stagehand/models/session_start_response.rb +55 -0
- data/lib/stagehand/models/stream_event.rb +120 -0
- data/lib/stagehand/models.rb +63 -0
- data/lib/stagehand/request_options.rb +77 -0
- data/lib/stagehand/resources/sessions.rb +488 -0
- data/lib/stagehand/version.rb +3 -1
- data/lib/stagehand.rb +74 -29
- data/manifest.yaml +17 -0
- data/rbi/stagehand/client.rbi +89 -0
- data/rbi/stagehand/errors.rbi +205 -0
- data/rbi/stagehand/file_part.rbi +37 -0
- data/rbi/stagehand/internal/stream.rbi +20 -0
- data/rbi/stagehand/internal/transport/base_client.rbi +314 -0
- data/rbi/stagehand/internal/transport/pooled_net_requester.rbi +83 -0
- data/rbi/stagehand/internal/type/array_of.rbi +104 -0
- data/rbi/stagehand/internal/type/base_model.rbi +308 -0
- data/rbi/stagehand/internal/type/base_page.rbi +42 -0
- data/rbi/stagehand/internal/type/base_stream.rbi +75 -0
- data/rbi/stagehand/internal/type/boolean.rbi +58 -0
- data/rbi/stagehand/internal/type/converter.rbi +216 -0
- data/rbi/stagehand/internal/type/enum.rbi +82 -0
- data/rbi/stagehand/internal/type/file_input.rbi +59 -0
- data/rbi/stagehand/internal/type/hash_of.rbi +104 -0
- data/rbi/stagehand/internal/type/request_parameters.rbi +29 -0
- data/rbi/stagehand/internal/type/union.rbi +128 -0
- data/rbi/stagehand/internal/type/unknown.rbi +58 -0
- data/rbi/stagehand/internal/util.rbi +487 -0
- data/rbi/stagehand/internal.rbi +18 -0
- data/rbi/stagehand/models/action.rbi +77 -0
- data/rbi/stagehand/models/model_config.rbi +94 -0
- data/rbi/stagehand/models/session_act_params.rbi +204 -0
- data/rbi/stagehand/models/session_act_response.rbi +250 -0
- data/rbi/stagehand/models/session_end_params.rbi +87 -0
- data/rbi/stagehand/models/session_end_response.rbi +30 -0
- data/rbi/stagehand/models/session_execute_params.rbi +440 -0
- data/rbi/stagehand/models/session_execute_response.rbi +414 -0
- data/rbi/stagehand/models/session_extract_params.rbi +209 -0
- data/rbi/stagehand/models/session_extract_response.rbi +91 -0
- data/rbi/stagehand/models/session_navigate_params.rbi +240 -0
- data/rbi/stagehand/models/session_navigate_response.rbi +91 -0
- data/rbi/stagehand/models/session_observe_params.rbi +198 -0
- data/rbi/stagehand/models/session_observe_response.rbi +184 -0
- data/rbi/stagehand/models/session_replay_params.rbi +89 -0
- data/rbi/stagehand/models/session_replay_response.rbi +286 -0
- data/rbi/stagehand/models/session_start_params.rbi +1703 -0
- data/rbi/stagehand/models/session_start_response.rbi +102 -0
- data/rbi/stagehand/models/stream_event.rbi +237 -0
- data/rbi/stagehand/models.rbi +25 -0
- data/rbi/stagehand/request_options.rbi +59 -0
- data/rbi/stagehand/resources/sessions.rbi +421 -0
- data/rbi/stagehand/version.rbi +5 -0
- data/sig/stagehand/client.rbs +41 -0
- data/sig/stagehand/errors.rbs +117 -0
- data/sig/stagehand/file_part.rbs +21 -0
- data/sig/stagehand/internal/stream.rbs +9 -0
- data/sig/stagehand/internal/transport/base_client.rbs +133 -0
- data/sig/stagehand/internal/transport/pooled_net_requester.rbs +48 -0
- data/sig/stagehand/internal/type/array_of.rbs +48 -0
- data/sig/stagehand/internal/type/base_model.rbs +102 -0
- data/sig/stagehand/internal/type/base_page.rbs +24 -0
- data/sig/stagehand/internal/type/base_stream.rbs +38 -0
- data/sig/stagehand/internal/type/boolean.rbs +26 -0
- data/sig/stagehand/internal/type/converter.rbs +79 -0
- data/sig/stagehand/internal/type/enum.rbs +32 -0
- data/sig/stagehand/internal/type/file_input.rbs +25 -0
- data/sig/stagehand/internal/type/hash_of.rbs +48 -0
- data/sig/stagehand/internal/type/request_parameters.rbs +19 -0
- data/sig/stagehand/internal/type/union.rbs +52 -0
- data/sig/stagehand/internal/type/unknown.rbs +26 -0
- data/sig/stagehand/internal/util.rbs +185 -0
- data/sig/stagehand/internal.rbs +9 -0
- data/sig/stagehand/models/action.rbs +46 -0
- data/sig/stagehand/models/model_config.rbs +56 -0
- data/sig/stagehand/models/session_act_params.rbs +111 -0
- data/sig/stagehand/models/session_act_response.rbs +121 -0
- data/sig/stagehand/models/session_end_params.rbs +41 -0
- data/sig/stagehand/models/session_end_response.rbs +13 -0
- data/sig/stagehand/models/session_execute_params.rbs +193 -0
- data/sig/stagehand/models/session_execute_response.rbs +215 -0
- data/sig/stagehand/models/session_extract_params.rbs +112 -0
- data/sig/stagehand/models/session_extract_response.rbs +36 -0
- data/sig/stagehand/models/session_navigate_params.rbs +114 -0
- data/sig/stagehand/models/session_navigate_response.rbs +36 -0
- data/sig/stagehand/models/session_observe_params.rbs +105 -0
- data/sig/stagehand/models/session_observe_response.rbs +89 -0
- data/sig/stagehand/models/session_replay_params.rbs +41 -0
- data/sig/stagehand/models/session_replay_response.rbs +137 -0
- data/sig/stagehand/models/session_start_params.rbs +866 -0
- data/sig/stagehand/models/session_start_response.rbs +44 -0
- data/sig/stagehand/models/stream_event.rbs +109 -0
- data/sig/stagehand/models.rbs +23 -0
- data/sig/stagehand/request_options.rbs +34 -0
- data/sig/stagehand/resources/sessions.rbs +121 -0
- data/sig/stagehand/version.rbs +3 -0
- metadata +170 -54
- data/.gitignore +0 -16
- data/Gemfile +0 -4
- data/Rakefile +0 -10
- data/lib/stagehand/client/oauth.rb +0 -32
- data/lib/stagehand/client/user.rb +0 -10
- data/lib/stagehand/rack/middleware.rb +0 -33
- data/lib/stagehand/railtie.rb +0 -19
- data/spec/spec_helper.rb +0 -7
- data/spec/stagehand_spec.rb +0 -44
- 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
|