diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c6a6955..7513895 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.5.2" + ".": "3.5.3" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index a0da57a..3eb04a2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml openapi_spec_hash: 8a36f79075102c63234ed06107deb8c9 -config_hash: 7386d24e2f03a3b2a89b3f6881446348 +config_hash: 4252fc025e947bc0fd6b2abd91a0cc8e diff --git a/CHANGELOG.md b/CHANGELOG.md index 819a91e..18c7a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.5.3 (2026-02-13) + +Full Changelog: [v3.5.2...v3.5.3](https://github.com/browserbase/stagehand-ruby/compare/v3.5.2...v3.5.3) + ## 3.5.2 (2026-02-07) Full Changelog: [v3.5.1...v3.5.2](https://github.com/browserbase/stagehand-ruby/compare/v3.5.1...v3.5.2) diff --git a/Gemfile.lock b/Gemfile.lock index d78caf4..b49924c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - stagehand (3.5.2) + stagehand (3.5.3) cgi connection_pool diff --git a/README.md b/README.md index 4089a35..ef6bbdd 100644 --- a/README.md +++ b/README.md @@ -136,16 +136,18 @@ bundle install Remote browser example: ```bash -export BROWSERBASE_API_KEY="your-browserbase-api-key" -export BROWSERBASE_PROJECT_ID="your-browserbase-project-id" -export MODEL_API_KEY="your-openai-api-key" +cp examples/.env.example examples/.env +# Edit examples/.env with your credentials. bundle exec ruby examples/remote_browser_example.rb ``` +The examples load `examples/.env` automatically. + Local mode example (embedded server, local Chrome/Chromium): ```bash -export MODEL_API_KEY="your-openai-api-key" +cp examples/.env.example examples/.env +# Edit examples/.env with your credentials. bundle exec ruby examples/local_browser_example.rb ``` diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 0000000..6272bb0 --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,4 @@ +STAGEHAND_API_URL=https://api.stagehand.browserbase.com +MODEL_API_KEY=sk-proj-your-llm-api-key-here +BROWSERBASE_API_KEY=bb_live_your_api_key_here +BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here diff --git a/examples/env.rb b/examples/env.rb new file mode 100644 index 0000000..fa3a3e3 --- /dev/null +++ b/examples/env.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ExampleEnv + REQUIRED_KEYS = %w[ + STAGEHAND_API_URL + MODEL_API_KEY + BROWSERBASE_API_KEY + BROWSERBASE_PROJECT_ID + ].freeze + + def self.load! + env_path = find_env_path + raise "Missing examples/.env (expected in repo examples/ directory)." unless env_path + + File.readlines(env_path, chomp: true).each do |line| + trimmed = line.strip + next if trimmed.empty? || trimmed.start_with?("#") + + key, value = trimmed.split("=", 2) + next if key.nil? || value.nil? + + ENV[key] ||= value + end + + missing = REQUIRED_KEYS.select { |key| ENV[key].to_s.empty? } + unless missing.empty? + raise "Missing required env vars: #{missing.join(', ')} (from examples/.env)" + end + + ENV["STAGEHAND_BASE_URL"] ||= ENV["STAGEHAND_API_URL"] + end + + def self.find_env_path + current = File.expand_path(Dir.pwd) + loop do + candidate = File.join(current, "examples", ".env") + return candidate if File.exist?(candidate) + + parent = File.dirname(current) + return nil if parent == current + + current = parent + end + end +end diff --git a/examples/local_browser_example.rb b/examples/local_browser_example.rb index ddedca0..8d432b4 100755 --- a/examples/local_browser_example.rb +++ b/examples/local_browser_example.rb @@ -4,8 +4,8 @@ require "bundler/setup" require "stagehand" -# Local mode runs the embedded Stagehand server and uses a local Chrome/Chromium. -# Set MODEL_API_KEY before running this script. +require_relative "env" +ExampleEnv.load! model_key = ENV["MODEL_API_KEY"].to_s if model_key.empty? warn "Set MODEL_API_KEY to run the local example." @@ -17,6 +17,40 @@ server: "local" ) +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error + error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error" + raise("#{label} stream error: #{error_message}") + end + end + result +end + session_id = nil begin @@ -35,21 +69,25 @@ client.sessions.navigate(session_id, url: "https://example.com") puts("Navigated to example.com") - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, instruction: "Find all clickable links on this page" ) - puts("Found #{observe_response.data.result.length} possible actions") + observe_result = stream_with_result("Observe", observe_stream) + observe_actions = observe_result || [] + puts("Found #{observe_actions.length} possible actions") - action = observe_response.data.result.first + action = observe_actions.first act_input = action ? action.to_h.merge(method: "click") : "Click the 'Learn more' link" - act_response = client.sessions.act( + act_stream = client.sessions.act_streaming( session_id, input: act_input ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, instruction: "extract the main heading and any links on this page", schema: { @@ -60,9 +98,10 @@ } } ) - puts("Extracted: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + puts("Extracted: #{extract_result}") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, execute_options: { instruction: "Click on the 'Learn more' link if available and report the page title", @@ -76,8 +115,13 @@ cua: false } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") + execute_result = stream_with_result("Execute", execute_stream) + execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil + puts("Agent completed: #{execute_message}") + puts("Agent success: #{execute_success}") + puts("Agent actions taken: #{execute_actions&.length || 0}") ensure client.sessions.end_(session_id) if session_id client.close diff --git a/examples/local_browser_playwright_example.rb b/examples/local_browser_playwright_example.rb index 587a8bf..77c7e8e 100755 --- a/examples/local_browser_playwright_example.rb +++ b/examples/local_browser_playwright_example.rb @@ -5,19 +5,8 @@ require "bundler/setup" require "stagehand" -# Example: Using Playwright with Stagehand local mode (local browser). -# -# Prerequisites: -# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable -# - Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID (can be any value in local mode) -# - Install Playwright (outside this gem): -# gem install playwright-ruby-client -# npm install playwright -# ./node_modules/.bin/playwright install chromium -# -# Run: -# bundle exec ruby examples/local_browser_playwright_example.rb - +require_relative "env" +ExampleEnv.load! begin require("playwright") rescue LoadError @@ -25,7 +14,7 @@ exit(1) end -model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +model_key = ENV["MODEL_API_KEY"] browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s diff --git a/examples/local_playwright_example.rb b/examples/local_playwright_example.rb index 55346ed..9001916 100755 --- a/examples/local_playwright_example.rb +++ b/examples/local_playwright_example.rb @@ -7,22 +7,8 @@ require "net/http" require "stagehand" -# Example: Using Playwright with Stagehand local mode -# -# This example mirrors the Python Playwright flow: launch the browser with Playwright, -# connect Stagehand to the same browser via CDP, and target the existing page by frame_id. -# -# Prerequisites: -# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable -# - Chrome/Chromium installed locally -# - Install Playwright (outside this gem): -# gem install playwright-ruby-client -# npm install playwright -# ./node_modules/.bin/playwright install chromium -# -# Run: -# bundle exec ruby examples/local_playwright_example.rb - +require_relative "env" +ExampleEnv.load! begin require("playwright") rescue LoadError @@ -69,9 +55,43 @@ def fetch_page_target_id(port, page_url) target_id end -model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error + error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error" + raise("#{label} stream error: #{error_message}") + end + end + result +end + +model_key = ENV["MODEL_API_KEY"] if model_key.to_s.empty? - warn "Set MODEL_API_KEY (or OPENAI_API_KEY) to run the local example." + warn "Set MODEL_API_KEY to run the local example." exit 1 end @@ -110,23 +130,27 @@ def fetch_page_target_id(port, page_url) session_id = start_response.data.session_id puts("Session started: #{session_id}") - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, frame_id: page_target_id, instruction: "Find all clickable links on this page" ) - puts("Found #{observe_response.data.result.length} possible actions") + observe_result = stream_with_result("Observe", observe_stream) + observe_actions = observe_result || [] + puts("Found #{observe_actions.length} possible actions") - action = observe_response.data.result.first + action = observe_actions.first act_input = action ? action.to_h.merge(method: "click") : "Click the 'Learn more' link" - act_response = client.sessions.act( + act_stream = client.sessions.act_streaming( session_id, frame_id: page_target_id, input: act_input ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, frame_id: page_target_id, instruction: "Extract the main heading and any links on this page", @@ -138,9 +162,10 @@ def fetch_page_target_id(port, page_url) } } ) - puts("Extracted: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + puts("Extracted: #{extract_result}") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, frame_id: page_target_id, execute_options: { @@ -155,8 +180,13 @@ def fetch_page_target_id(port, page_url) cua: false } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") + execute_result = stream_with_result("Execute", execute_stream) + execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil + puts("Agent completed: #{execute_message}") + puts("Agent success: #{execute_success}") + puts("Agent actions taken: #{execute_actions&.length || 0}") page.wait_for_load_state(state: "domcontentloaded") page.screenshot(path: "screenshot_local_playwright.png", fullPage: true) diff --git a/examples/local_watir_example.rb b/examples/local_watir_example.rb index 17bf64a..b79b801 100755 --- a/examples/local_watir_example.rb +++ b/examples/local_watir_example.rb @@ -7,20 +7,8 @@ require "net/http" require "stagehand" -# Example: Using Watir with Stagehand local mode -# -# This example demonstrates how to combine Watir (for low-level browser control) -# with Stagehand (for AI-powered actions) using a local browser. -# -# Prerequisites: -# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable -# - Chrome/Chromium installed locally -# - Install Watir (outside this gem): -# gem install watir -# -# Run: -# bundle exec ruby examples/local_watir_example.rb - +require_relative "env" +ExampleEnv.load! begin require("watir") rescue LoadError @@ -44,12 +32,46 @@ def fetch_cdp_websocket_url(port) ws_url end -model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +model_key = ENV["MODEL_API_KEY"] if model_key.to_s.empty? - warn "Set MODEL_API_KEY (or OPENAI_API_KEY) to run the local example." + warn "Set MODEL_API_KEY to run the local example." exit 1 end +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error + error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error" + raise("#{label} stream error: #{error_message}") + end + end + result +end + options = Selenium::WebDriver::Chrome::Options.new options.add_argument("--remote-debugging-port=#{DEBUG_PORT}") options.add_argument("--disable-gpu") @@ -82,21 +104,25 @@ def fetch_cdp_websocket_url(port) client.sessions.navigate(session_id, url: "https://example.com") - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, instruction: "Find all clickable links on this page" ) - puts("Found #{observe_response.data.result.length} possible actions") + observe_result = stream_with_result("Observe", observe_stream) + observe_actions = observe_result || [] + puts("Found #{observe_actions.length} possible actions") - action = observe_response.data.result.first + action = observe_actions.first act_input = action ? action.to_h.merge(method: "click") : "Click the 'Learn more' link" - act_response = client.sessions.act( + act_stream = client.sessions.act_streaming( session_id, input: act_input ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, instruction: "Extract the main heading and any links on this page", schema: { @@ -107,9 +133,10 @@ def fetch_cdp_websocket_url(port) } } ) - puts("Extracted: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + puts("Extracted: #{extract_result}") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, execute_options: { instruction: "Click on the 'Learn more' link if available", @@ -123,8 +150,13 @@ def fetch_cdp_websocket_url(port) cua: false } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") + execute_result = stream_with_result("Execute", execute_stream) + execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil + puts("Agent completed: #{execute_message}") + puts("Agent success: #{execute_success}") + puts("Agent actions taken: #{execute_actions&.length || 0}") browser.screenshot.save("screenshot_local_watir.png") puts("Screenshot saved to: screenshot_local_watir.png") diff --git a/examples/remote_browser_example.rb b/examples/remote_browser_example.rb index 336c7f1..17a2502 100755 --- a/examples/remote_browser_example.rb +++ b/examples/remote_browser_example.rb @@ -4,9 +4,8 @@ require "bundler/setup" require "stagehand" -# Remote browser example using Browserbase + Stagehand. -# Requires BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, and MODEL_API_KEY. - +require_relative "env" +ExampleEnv.load! browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s model_key = ENV["MODEL_API_KEY"].to_s @@ -27,6 +26,40 @@ model_api_key: model_key ) +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + if event.type == :system && event.data.respond_to?(:status) && event.data.status == :error + error_message = event.data.respond_to?(:error) && event.data.error ? event.data.error : "unknown error" + raise("#{label} stream error: #{error_message}") + end + end + result +end + session_id = nil begin @@ -39,12 +72,13 @@ client.sessions.navigate(session_id, url: "https://news.ycombinator.com") puts("Navigated to Hacker News") - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, instruction: "find the link to view comments for the top post" ) - actions = observe_response.data.result + observe_result = stream_with_result("Observe", observe_stream) + actions = observe_result || [] puts("Found #{actions.length} possible actions") action = actions.first @@ -55,13 +89,15 @@ puts("Acting on: #{action.description}") - act_response = client.sessions.act( + act_stream = client.sessions.act_streaming( session_id, input: action.to_h.merge(method: "click") ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, instruction: "extract the text of the top comment on this page", schema: { @@ -79,9 +115,10 @@ required: ["comment_text"] } ) - puts("Extracted data: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + puts("Extracted data: #{extract_result}") - extracted_data = extract_response.data.result + extracted_data = extract_result author = extracted_data.is_a?(Hash) ? extracted_data[:author] : nil author ||= "unknown" puts("Looking up profile for author: #{author}") @@ -91,7 +128,7 @@ "Click their username to open the profile and look for shared links." ].join(" ") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, execute_options: { instruction: instruction, @@ -106,9 +143,13 @@ } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") - puts("Agent actions taken: #{execute_response.data.result[:actions]&.length || 0}") + execute_result = stream_with_result("Execute", execute_stream) + execute_message = execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + execute_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + execute_actions = execute_result.is_a?(Hash) ? (execute_result[:actions] || execute_result["actions"]) : nil + puts("Agent completed: #{execute_message}") + puts("Agent success: #{execute_success}") + puts("Agent actions taken: #{execute_actions&.length || 0}") ensure client.sessions.end_(session_id) if session_id client.close diff --git a/examples/remote_browser_playwright_example.rb b/examples/remote_browser_playwright_example.rb index 89c2fda..3706caa 100755 --- a/examples/remote_browser_playwright_example.rb +++ b/examples/remote_browser_playwright_example.rb @@ -5,19 +5,8 @@ require "bundler/setup" require "stagehand" -# Example: Using Playwright with Stagehand remote mode (Browserbase browser). -# -# Prerequisites: -# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable -# - Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID -# - Install Playwright (outside this gem): -# gem install playwright-ruby-client -# npm install playwright -# ./node_modules/.bin/playwright install chromium -# -# Run: -# bundle exec ruby examples/remote_browser_playwright_example.rb - +require_relative "env" +ExampleEnv.load! begin require("playwright") rescue LoadError @@ -25,7 +14,7 @@ exit(1) end -model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +model_key = ENV["MODEL_API_KEY"] browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s diff --git a/lib/stagehand/version.rb b/lib/stagehand/version.rb index 6872959..42a2eb4 100644 --- a/lib/stagehand/version.rb +++ b/lib/stagehand/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Stagehand - VERSION = "3.5.2" + VERSION = "3.5.3" end